Verified against @dexterai/x402 5.3.1 · 2026-07-07
Sell with tabs
By the end of this page you will have one Express route that takes payment two ways from the same 402 challenge: a buyer's agent opens a tab and streams vouchers as you deliver, or a one-shot buyer pays for a single request. You price each request in your handler. You never call the chain on the hot path.
What a tab buys you as a seller
A per-call paywall makes the buyer settle a transaction before every request. That is fine for one big response. It is wrong for a token stream, where you deliver a thousand small chunks and one on-chain settlement per chunk would cost more than the chunk.
A tab moves settlement off the hot path. The buyer opens a tab once, then signs a small voucher for each unit you deliver. Each voucher is an ed25519 signature over a running cumulative amount. You verify it locally in microseconds. The money moves later, in one settlement, when the tab closes. The vouchers are the receipt you hold until then.
Install the one package you need. Sellers do not need a vault.
You also need a Solana RPC connection and your payout pubkey.
The whole seller in one route
tabOrExactMiddleware advertises both rails in a single standard x402 v2 challenge: scheme tab for agents that open a tab and stream, scheme exact for one-shot buyers and for catalog verifiers, which cannot open tabs. Put it on the route as the only payment middleware. In the handler, branch on how the request was paid.
requireTab(req) throws if the middleware did not run, so guard the exact rail first. perUnit on the middleware is the price you advertise in the 402 challenge. perUnit on openSse is what you actually charge per charge() call, and this is where per-request pricing lives. Set it from the request if your price varies: a longer prompt, a bigger page, a higher-resolution render. The middleware price is the sticker; the meter price is the bill.
meter.charge() fails closed. When the buyer's signed cumulative would exceed the cap on their session, or the session expired, or the voucher is not monotonic, charge() throws before send() runs. You stop delivering the moment you stop being paid.
Do not tune the crystallization cadence
tabMiddleware and tabOrExactMiddleware both take an optional lockCadence. Leave it unset.
Crystallization is the act of turning a signed voucher into an on-chain claim mid-stream, so a buyer who walks away without closing the tab cannot leave you holding nothing but signatures. As of 5.3.1 the facilitator owns that cadence. It runs a server-side engine that locks in every seller's delivered vouchers on its own schedule, whatever a client-side lockCadence says. If you set your own threshold, you are competing with an engine that already protects you, and a cadence-gated facilitator will refuse your sub-threshold locks with below_lock_cadence. The SDK records that refusal and stops re-asking, so nothing breaks, but the knob does nothing useful for you.
The default, when you omit it, is a delivered-amount threshold of 0.10 plus a lock at tab close. That is the seller-protection posture. The only reason to reach for lockCadence is to lock more aggressively than the facilitator on a tab you consider high-risk, and even that is being absorbed by the facilitator engine. Treat it as read-only.
lockCadence is a risk dial, not a performance dial. Lowering the threshold does not make you paid faster or paid more; the facilitator settles on its own cadence regardless. Setting it wrong only widens or narrows a window the facilitator already manages. Ship without it.
Restart-safe revenue
The default ledger is in memory. A process restart mid-stream loses the last in-flight voucher's worth of revenue on every open tab. For anything real, pass a durable ledger. FileChannelLedger writes one file per channel and survives restarts.
FileChannelLedger is enough for a single process at low-to-moderate concurrency. If you run more than one instance of your server behind a load balancer, the single-stream lease that stops two concurrent streams from over-delivering on the same tab is only atomic within one process. Either route every request for a given tab to the same instance, or back the ledger with a store whose acquire is atomic across processes (Redis SET NX PX, a Postgres advisory lock). Implement the ChannelLedger interface and pass your own.
Answer discovery for strangers
tabOrExactMiddleware already emits a discovery challenge. If you use tabMiddleware on its own (tab rail only, no exact fallback), a stranger's agent that arrives with no voucher gets nothing to resolve. Put tabChallengeMiddleware in front of it. It answers voucher-less requests with a standard 402 that advertises you as the counterparty, and lets requests that already carry a voucher fall through untouched.
How you observe that you were paid
Be clear about what the SDK gives you today, because it is less than a per-call seller expects.
While a tab is open, requireTab(req) hands you a SellerTab with two counters:
cumulative() is what the buyer has signed for. deliveredCumulative() is what you have delivered. Those are accrual counters, not settlement confirmation. The on-chain settlement itself is produced on the buyer's side: when the buyer closes the tab, their client posts the final voucher to the facilitator's settle endpoint, USDC moves from their vault to your token account, and the buyer receives the settlement signature.
The seller SDK does not expose a settle callback, a webhook, or a receipt stream. There is no onSettled event to subscribe to. To confirm money landed, watch your own USDC token account on chain, or reconcile against the facilitator. The Batch settlement page covers what the facilitator does with your vouchers between the last chunk and the settled transaction, including the buyer who streams and walks away. Until you have watched the account, treat "I hold valid vouchers" and "the money has settled" as two separate facts.