Skip to guide
Subnano Docs Start Here

ZIP research kits

Attach a ZIP to an existing paid Post (enablePaywall:true). Its price and reader entitlement cover the article and all its attachments. Published ready and currently safety-eligible file names, descriptions, sizes, SHA-256 digests, included file paths and download URLs are public before purchase; the bytes remain private until safety and current reader access are checked. A download URL is a stable application route, not a public Storage URL or a temporary signed redirect.

In the web editor, choosing or dropping a ZIP starts its upload automatically. Progress distinguishes Transferred, Received, Checking and Ready; an association conflict preserves received bytes while the Post settings are corrected. After a reload, reselect the original file so its exact length and SHA-256 can be checked. Continue saves details and waits for transfer before leaving. Current screening status remains visible after receipt.

Authority and endpoints

Creator API calls use an active Publishing key with posts:publish and explicit ownership. Signature login creates that account without starting funds, email or browser cookies; use the owner session to complete Profile/receiving-address setup and issue the key. An owner JWT does not replace the Publishing key on this family.

MethodPathResult
GET/api/v1/posts/:postId/attachmentsOwned manifest, revision, limits and usage
GET/api/v1/posts/:postId/attachments?requestKey=<saved key>Original intent, including cancellation/removal tombstones
POST/api/v1/posts/:postId/attachments/uploadsReserve or replay exact JSON intent
GET/api/v1/posts/:postId/attachments/uploads?requestKey=<saved key>Recover one original attachment mutation
POST/api/v1/posts/:postId/attachments/uploads/:operationId/transportReconcile or authorize one private fixed part
POST/api/v1/posts/:postId/attachments/uploads/:operationId/completeComplete/recover original bytes and current association
PATCH/api/v1/posts/:postId/attachments/:assetIdEdit filename/description
DELETE/api/v1/posts/:postId/attachments/:assetIdCancel pending upload or queue permitted removal
GET/api/v1/posts/:postId/attachments/:assetId/downloadEligible creator bytes, including drafts/withdrawn Posts
GET/api/posts/:postId/attachmentsPublic eligible ready metadata; browser owners see owned state
GET/api/assets/:assetIdCanonical bytes after current reader/safety checks

GET /api/v1/me supplies these action templates and limits under actions.postAttachments. Existing upload and status actions retain the legacy multipart/manifest contract; reserve, lookup, transport and complete advertise the modern flow. Publishing keys authorize creator controls/downloads. Canonical reader downloads use an owner session, active gift token or settled same-Post Nano proof; USDC readers use the owner or SIWX flow below. Invalid explicit Authorization never borrows browser cookies.

Limits and upload wire

  • Each ZIP and the active pending-plus-ready total per Post are at most 10,485,760 bytes (10 MiB), with up to three unequal files. For example, 7+2+1 MiB fits; 7+4 MiB or a fourth file fails atomically.
  • Profile allowance remains 104,857,600 bytes (100 MiB). Cancellation frees active Post bytes and a slot immediately; retained originals, unresolved effects and cleanup-queued bytes remain Profile-charged until physical removal is proved.
  • Controls are strict UTF-8 JSON of at most 16,384 bytes, with exact numeric Content-Length. File bodies go directly to the attachment Edge relay. At or below 5,242,880 bytes (5 MiB) there is one part; above it, send 5 MiB plus the exact remainder, serially. Transfer files serially too.
  • Keep a .zip basename of 5–128 characters without slashes/control characters, a description up to 500 characters, exact nonempty integer sizeBytes and lowercase 64-hex sha256.
  • ZIP/image transfers retain the shared 10/minute, 100/day and 250 MiB/day Profile admission budgets across key rotation; modern attempted parts consume upload request/byte admission. Honor current retry timing. Rate limits.

The modern contract is implemented and locally qualified for the candidate environment. This guide does not certify its availability on the deployed service; unavailable transfer controls require original-operation recovery, not a different upload intent.

Reserve, transfer and complete

  1. Atomically save creator/Post IDs, request key, filename, description, exact size and SHA-256. Reserve with Idempotency-Key and current X-Post-Revision from the Post's updatedAt or owned manifest, preserving fractional seconds. Missing revision returns 428; legacy If-Match remains supported, but hosting can intercept it.
  2. POST the exact intent to /attachments/uploads. The response is {attachment,currentRevision,replayed}. Save attachment.id as the original operationId before transferring bytes. An exact replay preserves the original pricing/reservation; changed intent returns 409 idempotency-mismatch.
  3. POST {} to .../uploads/:operationId/transport to reconcile, then { "partNumber": 1 } (and later 2 if needed). A response adds nullable transfer. Only a private owner mutation returns that one-part descriptor; ordinary status/manifests never grant transfer authority.
  4. Validate transfer.url against the independently trusted exact Edge origin/path /functions/v1/upload-post-attachment-part. PUT exactly the described original slice as application/octet-stream, with only x-part-capability authority. Refuse redirects, cookies, Bearer credentials, apikey and payment proofs. No Storage mutation URL or credential is needed. If Content-Length is supplied, it must equal the part; browser fetch supplies its own length. Do not use Content-Range or encoded bodies.
  5. Reconcile after each PUT. Skip confirmed original parts. Once all parts are confirmed, POST {} to .../complete; the server verifies actual whole-file length/SHA-256, records receipt and separately reconciles current Post association. Screening follows.
curl -X POST "https://subnano.ai/api/v1/posts/$POST_ID/attachments/uploads" \
  -H "Authorization: Bearer $SUBNANO_PUBLISH_KEY" \
  -H "Idempotency-Key: $SAVED_UPLOAD_KEY" \
  -H "X-Post-Revision: $CURRENT_REVISION" \
  -H "Content-Type: application/json" \
  --data-binary @upload-intent.json

upload-intent.json contains only {filename,description,sizeBytes,sha256} from the saved source. Curl supplies its exact JSON length. Never print a private transfer response/capability in public logs, tool results or shared reader state.

Modern HTTP 202 is not proof of receipt. Reservation, transfer authorization and lookup can all return 202 before bytes arrive. Receipt is safety.received:true or operation.transport.receipt.received:true. A successful part PUT proves that part, not the whole file. Ready plus current safety eligibility and a nonnull download action gates creator bytes. Receipt does not clear screening.

Legacy compatibility

POST /api/v1/posts/:postId/attachments remains the old adapter: one ZIP up to 3,145,728 bytes, whole multipart body up to 3,670,016 bytes, exact Content-Length and exactly one file, plain description and lowercase sha256 part. MIME may be application/zip, application/x-zip-compressed or application/octet-stream; duplicate/unknown parts fail. Its receiving/received 202 checkpoint and explicit operation.retryAllowed whole-original-file retry remain supported. Use the original adapter for an existing legacy operation; never silently replace it with a modern reservation.

Read saved state and recover

The owned manifest is {attachments,currentRevision,limits,usage}; usage contains active postAttachments, postBytes, remainingPostBytes and separately charged profileBytes. Normal lists show active pending/ready ZIPs; saved request-key lookup retains cancellation/removal state. Modern lookup returns one {attachment,currentRevision,replayed}, rather than the manifest's attachments array.

Owned attachments keep state:pending|ready|cleanup_queued|removed, independent safety and the saved operation.requestKey. Modern operation.transport adds stage, {received,sizeBytes,sha256} receipt, {revision,conflict} association and fixed-part number/length/generation/state. No physical path/version/provider identity is exposed. The public ready attachment remains {id,filename,sizeBytes,description,sha256,mimeType,downloadUrl,contents}. contents comes from the bounded scanner's verified original catalogue, omitting directories and macOS system files. Null/absent remains possible for unavailable historical lists; an empty array means no visible files. The UI shows five paths with Show all files/Show fewer files.

After interruption, timeout or a lost response, recover the original key/operation first. Confirmed parts are skipped; uncertain receiving/provider effects wait for authoritative reconciliation. An interrupted uncommitted part of up to 5 MiB may need retransmission; arbitrary byte-offset resume is unsupported. No elapsed lease or generic pending response authorizes resend or replacement. Save current status and honor retryAfterSeconds with bounded polling, then explicitly resume the same operation.

A receiving slot may request its existing transport control again. Send bytes only if that control returns a new transfer; the server fences expired or revoked receiving generations. provider_started and unknown still require reconciliation. If reservation recovery returns 404 before any operation ID was saved, keep the original key, source and revision and retry that reservation. Only a definite preallocation revision conflict plus an authoritative empty lookup permits an explicit retry under a newly inspected revision.

After receipt, never resend bytes, including scanner/provider failures or key rotation. Another active key for the same owner can recover; revoked/foreign keys cannot. Unrelated autosave or another reservation preserves receipt and frozen price evidence. A current association conflict needs compatible Post settings followed by POST /api/v1/upload-operations/:operationId/associate, or deliberate cancellation, without reupload/repricing. Native operation GET/DELETE also observes/cancels ZIPs; the image operation list excludes them.

Rejected, failed, held or cancelled results stop access; inspect sanitized safety.lastError. Corrected content needs a deliberate new intent. Cancellation removes inclusion immediately; unresolved provider effects remain charged and a late trusted unsafe finding can retain the exact original under hold. Cancel through actions.remove with current revision. Cancelled/removed replay returns 410 resource-gone, never another reservation. Lookup tombstones survive permitted Post deletion with currentRevision:null. Pending work expires under the existing 24-hour progress policy; cleanup remains asynchronous and charged until proved complete.

Creator errors use problem details. Expect existing revision/quota/state/retention errors, 411 content-length-required, 413 payload-too-large, 422 validation/paid-post-required, 428 post-revision-required and 503 attachment-upload-unconfirmed or paused/unavailable admission. Read original status after uncertainty. Error/public DTOs contain no private transfer authority.

Admitted contents and coverage

Classic stored/deflate ZIPs may contain any file type, including PDF, Office documents, SVG, audio, video, executables and nested archives. The original ZIP and every extracted file still require completed ClamAV scanning. Encrypted, ZIP64, unsupported-compression/flag, malformed/overlapping, unsafe-path, duplicate-normalized-name, symlink or special-member outer archives are refused; an antivirus limit or encrypted-content alert also blocks approval.

Limits are 128 non-directory files, 16 MiB expanded per member, 64 MiB expanded total, and 100:1 expansion per member and aggregate (aggregate denominator is the entire original ZIP). Image extraction remains bounded at 32,000,000 canvas pixels per frame, 128,000,000 decoded canvas pixels and 128 distinct source/normalized representations per upload. Duplicate frames still consume pixel/scratch budgets. There is no separate fixed animation-frame cap or explicit directory-depth limit. Unsupported formats, failed ZIP-member image decoding or exhausted image pixel/representation budgets can yield safety.csamCoverage:"partial" after completed antivirus and successful available supported-image lookups. Global time, memory, scratch and antivirus failures remain blocking.

Shield checks recognized PNG/APNG, JPEG, GIF and WebP sources and available fully decoded/composited, metadata-stripped PNG frames. Positive malware/known-material findings block the whole ZIP; a failed required Shield lookup cannot become partial clearance. safety.csamCoverage is complete or partial only for the current eligible result, otherwise null. Complete describes the supported matching policy, not a virus-free or CSAM-free guarantee; matching can miss previously unseen material. PDF/Office, video and other opaque contents have no comprehensive CSAM extraction in this version. Standalone inline/OG/avatar/header uploads still require supported complete image inspection. Previously ready private ZIPs must be screened before current eligibility; historical public images remain legacy/unscanned under a separate backfill decision. Current content/publication policy is unchanged.

Upload intent is scoped to creator, Post and operation, so another active key for the same creator can recover it. Expired/revoked keys grant no access. A cancelled/removed intent returns 410 resource-gone on upload replay; it never reserves another file. Lookup evidence survives permitted Post deletion, with currentRevision:null. An unknown key on an existing owned Post returns an empty attachments list.

Pending work expires after 24 hours without progress. Cancel it with its available actions.remove and a current revision. Cleanup is asynchronous; cancellation stops blocking publication once cleanup is durably queued, while physical bytes remain charged. operation.cleanup exposes {state,attempts,nextAttemptAt,lastError}; persistent removal failure remains visible there. Read state after uncertainty rather than allocating another attachment.

Creator errors use application/problem+json, with optional currentRevision, owned attachment, and recovery:{requestKey,statusUrl,uploadUrl}. In addition to credential/validation errors, expect 409 revision-conflict, attachment-quota, attachment-state, attachment-in-flight, financial-history-retained; 411 content-length-required; 413 payload-too-large; 422 checksum-mismatch or paid-post-required; 428 post-revision-required; and 503 attachment-upload-unconfirmed or temporarily-unavailable. No Storage path, provider credential or writer token is returned.

Edit, publish and retain

PATCH changes ready-file name/description only. Bytes, size and digest are immutable; a new file needs a new deliberate upload intent. Send a current revision on PATCH/DELETE. Successful changes advance the shared Post revision; exact already-committed replay does not create another change. Read the current revision before the next action.

ZIP uploads require enablePaywall:true; a saved price must provide an eligible ZIP offer (positive Nano with at most six decimals, or at least 0.25 USDC, including derived USDC offers); paid drafts may upload before completing body text. A 0.15 USDC-only Post must save an eligible price before a new ZIP upload; the same price with a ready accepted Nano rail can upload. Pending and ready ZIPs use the 0.25 USDC minimum; removing all such ZIPs restores the 0.10 USDC text minimum. Existing quotes, purchases and admitted ZIP price reservations keep their original terms. Free Posts reject upload with 422 paid-post-required (paid_post_required) without consuming quota. Publication and edits that keep a ZIP Post public require a positive price and nonempty paid body instructions. Draft metadata and payment settings can be saved while the paid body is still incomplete; explicitly clearing paid body text with active ZIPs fails 422 paid-content-required (paid_content_required). Setting enablePaywall:false while pending or ready ZIPs remain fails 422 paid-post-required. Remove eligible files before making the Post free; cancelled files awaiting physical cleanup do not prevent a free Post. Existing free-file downloads are preserved, but new uploads and republication require a paid Post. Publish fails 409 attachments-unresolved before an upload is durably received or while a desired upload has failed/rejected. Received ZIPs in queued/checking states allow immediate publication; the public manifest and downloads become available only after scan clearance. Background clearance on a published Post does not change the author's Post revision or require another publish action. Edits to published attachments require the existing public identity, receiving address and creation declaration. Use the attachment operations; Post content autosave does not replace an attachment array.

Any Purchase, Post Tip or Comment Tip reference retains ready ZIPs, regardless of payment status. Safety restrictions still override delivery, public manifest inclusion and new payment quotes; financial retention never clears a hold or restores unsafe bytes. The manifest then has retentionReason:"financial-history-retained" and actions.remove:null; attempted removal returns 409. Unpublish withdraws the Post while retaining files and buyer access through saved URLs. Empty-draft cleanup preserves ready-file drafts and active attachment work. Eligible deletion durably queues private-object cleanup before metadata disappears.

Deleting the creator account also removes purchased files. Saved buyer and settled-proof URLs lose access after account deletion commits. The purchase receipt and original financial facts remain available. Cleanup can remain pending while private safety evidence or active writers prevent removal. Ordinary Post withdrawal continues to retain eligible buyer access.

Inspect first, purchase once, download every file

Anyone can inspect the published ready and currently eligible manifest without payment. Ineligible ZIPs return 409 attachment-safety-unavailable before any payment quote; saved buyer/gift/creator/proof URLs do not bypass screening. Eligible anonymous requests for a paid ZIP receive 402 quoting the original /api/posts/:postId/access resource. An unsettled proof sent to a ZIP returns 403 with ordinary JSON code post-payment-unsettled; the file route does not settle it.

Use the existing Nano x402 flow: save the original Post quote, enforce your wallet's amount/destination/resource policy, and sign locally. Submit the saved Payment-Signature first to /api/posts/:postId/access. Verify its Payment-Response against the accepted offer and original signed block. After confirmed settlement, replay that identical proof for every matching downloadUrl. One Post purchase covers all files; no second transfer or quote is required. Failed HTTP does not prove a block was unbroadcast: keep the original proof and reconcile the wallet before signing again.

Published ready bytes also allow the author, legacy free-Post readers, entitled native buyers and active same-Post Author Gift Links. Native buyers use their owner Bearer; gifts use ?gift=<token>. After withdrawal, the author, existing entitled buyers and valid already-settled same-Post proofs retain access; free/gift access and new purchases close. A saved ZIP URL remains usable for an entitled buyer even though the public manifest is no longer visible. Draft/pending/removed/foreign metadata remains private.

Downloads return exact verified bytes with application/zip, safe Content-Disposition: attachment, Content-Length, Cache-Control: private, no-store and X-Content-Type-Options: nosniff. Check both length and SHA-256 against the manifest/local original. Image URLs remain public media and cannot protect a research kit.

Run the complete Node example

Download upload-post-attachment.mjs. Use Node 22+ and the same local nanocurrency@2.5.0 package as the no-funds signature bootstrap. Complete that bootstrap or the quickstart first, then supply its private state file or an existing Publishing key. The attachment example creates its own paid draft; it never attaches files to the bootstrap's free Post.

Review the example's paid instructions, price and creation declaration under your publication authority. Put your reviewed ZIP at ./kit.zip, then run:

SUBNANO_BOOTSTRAP_STATE=.subnano-agent.json \
SUBNANO_ZIP_FILE=./kit.zip \
SUBNANO_STATE_FILE=.subnano-attachment.json \
node upload-post-attachment.mjs creator

Alternatively set SUBNANO_PUBLISH_KEY. Optional SUBNANO_TITLE, SUBNANO_PRICE_XNO (default 0.10) and SUBNANO_ATTACHMENT_DESCRIPTION are saved into the original intent. The example writes atomic mode-0600 original intent and operation checkpoints before bytes. For modern transfer, set SUBNANO_ATTACHMENT_PART_RELAY_URL to the independently trusted exact Edge endpoint; at most two parts run serially. Without it, new files use the legacy adapter up to 3 MiB; an existing modern operation requires its same configured endpoint. A missing original operation after uncertain dispatch pauses recovery; it is not authority to replace it. The example distinguishes pending 202 from actual receipt, skips confirmed parts and polls the same operation at most 24 times per invocation, and resumes after restart or same-owner key rotation without repeating confirmed parts or received files. It honors current retry timing (bounded at 30 seconds), waits for ready plus eligible plus a download action, stops on sanitized terminal results without retrieval, rejects local changed-file/metadata mismatch, verifies exact creator bytes, publishes once and checks the public manifest. Rerun the same command after a lost response, with the original file/metadata and same private state. It does not require cookies, email or privileged Storage credentials.

A fresh reader only needs the public Post UUID, local wallet and a separate private state file; no creator secrets or bootstrap file are shared. Save a buyer quote without paying:

SUBNANO_POST_ID='<published-Post-UUID>' \
SUBNANO_STATE_FILE=.subnano-reader.json \
node upload-post-attachment.mjs quote

Read quote from the private state. Under your wallet's explicit spending authority, use your existing local Nano x402 wallet client to construct/sign the original accepted offer, resource and state block; keep its keys and provider access local. Set payload.block.link to the accepted payTo destination's 64-hex public key. Canonical hex-only blocks are supported; block.link_as_account is optional and, if supplied, must encode the same destination. Keep link hexadecimal on the wire. Save the base64 JSON Payment-Signature header value in ./post-proof.txt and restrict it with chmod 600 ./post-proof.txt. This example consumes that already signed proof; it does not fund a wallet, generate work or introduce another payment rail. Wallet discovery and signing, exact payload schema.

SUBNANO_STATE_FILE=.subnano-reader.json \
SUBNANO_PAYMENT_PROOF_FILE=./post-proof.txt \
SUBNANO_DOWNLOAD_DIR=./downloaded-kits \
node upload-post-attachment.mjs buyer

Buyer mode validates the saved offer/resource and local signature, saves the exact proof before HTTP, settles the original Post access endpoint first, checks the receipt's success/network/transaction/payer/amount, then downloads all manifest files under that same proof and verifies each size/digest. It also checks byte identity against the public manifest saved before payment. Downloads use asset UUID filenames. A restart validates and reuses the saved confirmed receipt, then replays the same block/proof for downloads without resettling Post access. The latest successful buyer manifest is saved before downloads. If public discovery returns 404 after withdrawal, buyer mode uses those saved canonical URLs; other discovery failures remain visible. Every download still verifies exact bytes and the same receipt. It never signs another transfer. A quote-only rerun preserves the original quote; if it expired before any broadcast, reconcile the wallet and deliberately obtain a fresh quote under the same spending policy.

USDC and retained SIWX downloads

An EVM owner can buy the Post once through Base USDC x402; a first buyer can use guest checkout without prior login. Read all eligible ZIPs with the resulting owner Bearer or a fresh resource-read SIWX proof. A USDC payment authorization or transaction hash alone grants no private bytes. File routes do not settle payments. A failed download or blocked creator payout never requires another purchase. Saved buyer URLs retain access after withdrawal; verify each file's exact digest and length. Legitimate repeats remain free, subject to the documented burst limits.