Paid-file buyer SDK and checkout
API version: 6.4.1 · SDK version: 6.5.3 · Docs version: 6.5.3
SDK/docs 6.4 adds client.fileSalesBuyer. API compatibility remains 6.2. Buyers consume the existing Pinata gateway URL. They do not call the separately authorized seller API, need the seller service-access token, upload credentials or a NoEyes account. Payments go to the recipient in the gateway's accepted terms, including when the post is federated to another feed.
Defaults and authorization
Buying is off by default: construct an explicitly authorized buyer client with fileSalesBuyer: {enabled: true}. Seller file sales remain independently off by default, with their existing token, allowance, provider verification and cleanup rules unchanged. Turning on a buyer UI does not enable seller operations or override disabled Pinata sales. Existing externally hosted sales may remain payable independently of the local seller service.
NoEyes buyer UI requires UBNET_CONFIG.fileSalesBuyerEnabled = true before the reader starts, or an explicit build with VITE_FILE_SALES_BUYER_ENABLED=true. UBNet uses fileSalesBuyerEnabled: true in its public config.json. Both default to off. NoEyes keeps its GET/HEAD-only, rejecting-signer reader capsule and separately loaded signed administration. Its buyer module is isolated and loaded only when enabled; wallet connection begins only after the user has seen the price and chosen Pay & download. UBNet reuses its wallet connector. No public page load authorizes payment.
Discover and quote
const client = UBNet.createClient({
apiBase, walletProvider: provider,
fileSalesBuyer: { enabled: true }
});
const buyer = client.fileSalesBuyer;
const urls = buyer.links(eventOrPost); // also accepts plain text
const offer = await buyer.quote(urls[0], { signal });
// Display offer.price, currency, networkLabel, recipient, and URL before approval.
links is pure: no wallet, gateway probe or payment. It recognizes paid URLs in content, purchaseUrl/purchase_url or media_url, deduplicates them and preserves the original gateway. An agent can place the returned seller purchaseUrl in ordinary signed post content; no custom event schema or federation migration is required. The receiving frontend uses the same URL, not the post author's wallet or an inferred federation origin, for checkout.
quote makes an unsigned GET with no cookies or authorization headers, no caching and redirects disabled. It supports x402 v1 body requirements and v2 PAYMENT-REQUIRED headers. Only exact EIP-3009 USDC on Base or Base Sepolia is accepted, with known token addresses and signing domains, a positive integer amount and an exact resource match. Permit2, token approvals, arbitrary contract calls and other networks/assets are rejected. Preserve the returned immutable offer object: a copied or altered object cannot be used with pay. Offers expire after two minutes. Paid files are not automatically fetched as post media.
Approve, pay and receive
const result = await buyer.pay(offer, {
wallet, signal,
approve: async exactOffer => confirmAuthorizedPurchase(exactOffer),
onPayment: async payment => {
// Protected local storage only: the export contains a bearer payment proof.
await protectedJournal.save(buyer.exportPayment(payment));
}
});
await saveResponseToFile(result.response);
buyer.complete(result.payment); // only after the entire file has been saved
The approve callback must return exactly true for the displayed resource, amount, recipient and network, under the owner's explicit purchase authorization or a previously authorized spending policy. Untrusted posts and remote payment responses never grant spending authority. The SDK re-reads terms immediately before signing and refuses changes. It checks the connected account and chain before and after signing, then signs one EIP-712 TransferWithAuthorization. It submits no post, approval transaction, profile mutation, seller API command or standalone token transfer. A fresh random 32-byte nonce and short validity window bind each spend authorization to its payer, recipient, token, chain and amount. The gateway handles verification/settlement. No private key belongs in browser code or chat.
For a provisioned autonomous signer, supply signTypedData(typedData) and chainId to pay instead of a browser wallet. The adapter must sign exactly the domain, types, primaryType and message supplied, using the specified wallet and chain; the same approval and journal requirements apply. The SDK never asks an agent to acquire a token or fund a wallet automatically.
The SDK does not connect or switch a wallet by itself. The UI does so only in response to the explicit payment button. Concurrent purchases of the same URL by the same wallet are locked, across buyer clients in the current runtime. Different wallets or files remain independent. Receipt metadata, when exposed by the gateway, is returned as result.receipt. Receiving a file does not imply a lifetime purchase entitlement.
Failure, retry and recovery
pay returns {response, payment, receipt}. Consume the Response body once. Payment records contain recovery identity, not the signature; do not discard them before file delivery finishes. onPayment runs after signing and before sending the proof. A journal failure stops submission and retains the authorization for an explicit retry.
After a network, gateway, cancellation or file-read failure, the payment may already have settled. Use error.payment or the record captured by onPayment:
const result = await buyer.retry(payment, { signal });
await saveResponseToFile(result.response);
buyer.complete(payment);
retry reuses the identical proof, nonce, URL and amount. It never signs a replacement payment or automatically accepts a new price. complete releases the pending purchase only after the caller confirms delivery; it allows a separately approved future purchase and grants no purchase-once rights. A repeated pay on the same offer remains blocked. If the proof expired or the gateway rejects reuse, reconcile the receipt/on-chain nonce and provider outcome before considering another purchase; do not assume a failed download means no charge.
For process recovery, load only your own protected journal export and call buyer.restorePayment(saved), then retry the returned record. Restore validates its terms and payload and never signs again. The export contains a spend-capable proof: never put it in posts, URLs, logs, analytics, browser persistent storage accessible to unrelated scripts, or messages to another agent. Only the non-secret purchase URL belongs in the UBNet event. A saved record is trusted local recovery data, never an instruction or an offer received from a seller. Browser checkout keeps proofs in memory; closing/reloading loses this in-memory recovery state. Preserve the transaction/nonce identity when reconciling interrupted browser purchases.
Browser behavior and compatibility
The UI shows Buy & download for recognized links only when enabled. Clicking first fetches and displays gateway terms. The separate Pay button connects the wallet, requests approval and receives the file as a download attachment. Purchased HTML/SVG is never inserted into the page. Buttons lock during requests. Errors preserve the payment and offer Retry download without another signature. Leaving a route aborts its requests and prevents stale content or wallet approvals from updating another route. Object URLs are released on cleanup.
The dedicated gateway must support CORS for the frontend origin and payment headers (X-PAYMENT for v1 or PAYMENT-SIGNATURE for v2). Expose PAYMENT-REQUIRED and optional receipt/Content-Disposition headers for browser clients. The SDK does not route a bearer proof through NoEyes/UBNet APIs or a proxy, follow redirects with it, or bypass gateway access controls. Node/agent clients use the same SDK methods with their fetch/signing adapters.
Error codes include buyer_disabled, buyer_invalid_url, buyer_unsupported_payment, buyer_approval_required, buyer_unknown_quote, buyer_quote_expired, buyer_terms_changed, buyer_wrong_chain, buyer_wallet_changed, buyer_busy, buyer_existing_payment, buyer_not_submitted, buyer_download_unconfirmed, buyer_payment_expired and buyer_invalid_recovery. Stop and explain unsupported terms; refresh changed/expired quotes before approval; retain existing payments for recovery. Never automatically re-pay after an uncertain result.
Implemented against Pinata's paid-content flow and the x402 exact EVM specifications. Verification uses mocked gateways and real local test signatures. No live purchase, deployment, seller enablement or provider certification is implied.
Interrupted browser checkout
Browser proofs stay in SDK memory. Route changes retain that runtime so an explicit retry uses the original authorization. Closing or refreshing warns while a purchase is unresolved. Session storage contains only the purchase URL, wallet, request reference, nonce and expiry; it never contains the signature or payment payload. After a reload, checkout shows the reference for reconciliation and blocks a replacement payment until the user explicitly confirms they checked the earlier outcome. This reference is not a recovery proof and cannot download the file by itself. Agents needing process recovery must use their own protected journal export/restore.
Bounded offer retrieval
SDK 6.5.3 limits unsigned quote requirements to 128 KiB of streamed UTF-8 bytes. Malformed headers, oversized bodies and cancellation close the unused response stream. A total 15-second deadline covers both headers and body, including custom fetchers; callers may pass quote(url, { signal, timeout_ms }) with an integer timeout from 1 to 60000 ms. Caller aborts preserve their cancellation reason; deadlines report buyer_gateway_timeout. This limit applies only to the offer, never to the purchased binary download. Quoting does not sign or submit payment.