← Blog
November 1, 2026 · 13 min

x402 conversation budgets on Solana: a prepaid key with an escrow behind it

A chat app cannot ask the user to sign a wallet popup for every message. An open x402 pull request proposes a fix: approve one capped budget, then talk. We read the 856-line spec, reproduced its hash vector and checked what the credential, the cap and the refund path actually guarantee.

The proposal is PR #4068 on x402-foundation/x402. It was opened on 2026-10-30 by notorious-d-e-v, it is spec-only, and its head commit when we read it was 2d05e55. It had no reviews. It adds one new file, scheme_batch_settlement_svm_conversation_budget.md, and edits the parent Solana batch-settlement binding (899 lines added, 12 removed across the two files).

We covered the channel machinery in the batch-settlement deep dive and the custody question in yesterday's voucher custody audit. This post is about a different layer: the authorization a browser user gives once, and what it can and cannot bound.

What the PR proposes

The flow has five moving parts.

  1. One channel, one deposit. The channel is fresh and funded to exactly the approved maximum. The spec says an existing larger channel "cannot be treated as an equally protected smaller budget".
  2. A private credential. The browser generates 32 random bytes, written as 64 lowercase hex characters. Only its SHA-256 is public. It rides in the PAYMENT-SIGNATURE header of every later request.
  3. A native opening that commits the terms. The payer-signed open transaction carries a Memo with the budget ID, the cap, the expiry and a digest of the full terms. The terms include the audience origin, network, payer, operator, receiver, sponsor, asset, salt, open slot, withdrawal delay and the credential hash.
  4. Server-signed cumulative vouchers. The merchant reserves a ceiling before the handler, runs it, then commits the actual charge and the next cumulative voucher. Admission is a merchant ledger decision, not a chain call.
  5. Recovery by the existing refund. A lost credential is handled by the payer wallet signing the existing zero-charge close intent. No new recovery endpoint exists in v1.

The mode is negotiated with a new field. A facilitator lists "conversation-budget-v1" in extra.authorizationModes. The merchant's 402 sets extra.authorizationMode, plus paymentFlow: "escrow" and voucherSigner: "server". A client that does not know the mode must skip that accept, never read it as legacy. The merchant also advertises a second, conventional accept on the same route, used only for refunds.

The first request is one ordinary 402 and one signed retry, with a budget-deposit payload. Later requests send budget-authorization with the budget ID, a fresh requestId and the credential. The facilitator never sees the credential: the merchant forwards only a sanitized public variant.

Reproducing the vector

The spec ends with a non-payment fixture: synthetic public keys, a credential of 64 ASCII zeros and the expected hashes. A format that commits a secret hash and a terms digest lives or dies on byte-exact serialization, so we recomputed it from scratch in Python.

# credential = "0" * 64
sha256(credential)   = 60e05bd1b195af2f94112fa7197a5c88289058840ce7c6df9693756bc6250f55
sha256(terms bytes)  = 15ad92f2f521b188bdf4414aa4658b033f832c71bcce0333b9b729339ca50234
termsHash (b64url)   = Fa2S8vUhsYi99EFKpGWLAz-DLHG8zgMzubcpM5ylAjQ   # 43 chars
Memo length          = 145 bytes
Memo at max fields   = 164 bytes   # cap 2^64-1, expiry 2^53-1

All four values match the Appendix, including the 164-byte worst case, which sits well under the existing 256-byte Memo limit. The terms are 18 fixed lines joined by LF with no trailing newline, so a wallet that normalizes line endings or trims whitespace breaks the digest. The spec knows this: it rejects uppercase hex, CRLF and changed case, and its acceptance matrix lists "MetaMask, Phantom, Solflare" as an unverified compatibility row. We could not test any wallet.

The commitment design is careful. Because the credential hash is inside the signed terms, a merchant cannot swap in another hash after the user approves. Because the audience origin is inside the terms too, a credential approved for one merchant does not validate against another.

What the PR does not change in the SDK

We cloned the repository and grepped for authorizationMode, conversation-budget and budget-deposit. They appear only in the two spec files. The Solana batch-settlement code in typescript/packages/mechanisms/svm/src/batch-settlement already has a voucherSigner option and a maxIdleSecs field, so the server-signed base exists. The budget mode does not.

The PR's own notes say as much: unmodified SDK versions are not claimed to support it. One detail matters for chat. The current refund helper in client/refund.ts probes the route and sends the signed refund with method: "GET". A chat endpoint is a POST route. The spec answers this by requiring the merchant to dispatch refund control messages at the resource URL "independently of the paid handler and its method restrictions". That is a server-side obligation the helper does not yet exercise.

A related PR, #3905, adds opt-in readable batch proofs for browser signers. It is separate and still open. The budget spec leans on it indirectly: a wallet that cannot sign the existing binary refund proof has no cooperative HTTP recovery.

Finding 1: the cap is a promise, the deposit is the bound

The word "budget" suggests a limit enforced by the system. The spec is explicit that it is not: "Expiry and Stop are merchant admission controls; the operator retains its existing onchain authority over the deposit." The voucher signer is the server, and the server can claim up to the full deposit onchain.

So the real guarantee is the escrow size. A dishonest merchant can take the whole amount. It cannot take more. That is a strictly better bound than an open-ended allowance, and it is the reason for the one-channel-one-deposit rule. It also means the user locks the whole cap up front. A 5 USDC budget is 5 USDC out of the wallet on the first message, not 5 USDC of permission.

Compare this with the allowance model in our Spend Permissions audit, where the wallet keeps funds and an onchain contract enforces a period limit. The conversation budget gives up that enforcement to get one popup. The trade is honest but it should be named: this is a prepaid balance with a refund, not a spending policy.

Finding 2: a stolen credential drains, but cannot steer

The credential is a bearer secret. Whoever holds it can spend the remainder. The spec says so directly and bounds the damage with expiry, Stop and honest accounting. Two design choices make the failure mode narrower than it first looks.

That ordering rule is the part of the spec most likely to be implemented wrong. "Verify, then check busy, then stop" leaves a window where a thief keeps admitting work while the owner's refund is rejected as busy. The acceptance matrix has a row for it: "Refund while stolen credential submits concurrent work: durably stop before checking busy".

The spec lists where the credential must never appear: URLs, onchain data, facilitator requests, logs, analytics, model or tool inputs, and unrelated redirects. For an LLM product the last of those is not abstract. A header that carries a spendable secret sits next to prompts and tool output in many tracing pipelines.

Finding 3: reserving the ceiling strands part of the budget

Admission is one inequality:

spent + reserved + next request ceiling <= approved maximum

The ceiling is accepted.amount, the quoted per-request maximum. Actual cost can be lower, and the unused part is released. But the last request is admitted only while the remainder is at least one ceiling. Whatever is left below that cannot be spent. It is not lost, because the refund returns it, but spending it down to zero needs a wallet signature.

The worst-case stranded amount is just under one ceiling. On a 5 USDC budget:

Quoted ceilingMax strandedShare of 5 USDC
0.01 USDCunder 0.010.2 percent
0.10 USDCunder 0.102 percent
0.50 USDCunder 0.5010 percent
1.00 USDCunder 1.0020 percent

This is arithmetic, not a measurement. It matters for LLM billing because the ceiling is usually derived from a token cap, and a long-context or reasoning request can have a ceiling many times its typical cost. The spec also says a client must obtain fresh requirements if pricing changed, and that echoed offers cannot redefine the grant.

We probed our own gateway's free 402 on 2026-11-01 for four model slugs across max_tokens from 256 to 200,000. Every response was an exact quote of 10,000 atomic USDC (0.01 USDC) on Base. That flat value looks like a floor, so those probes cannot show how a real ceiling would scale. What they do show is the second field: the 402 advertises maxTimeoutSeconds of 300.

Finding 4: one operation in flight means one channel per parallel call

V1 clients "MUST keep at most one logical paid operation in flight per channel, including the first funded resource". That is sensible for a human chat, where turns are sequential. An agent is different. A planner that fans out eight tool calls in parallel has two options under this spec: serialize them, or hold eight channels.

Eight channels at a 5 USDC cap each is 40 USDC escrowed and eight opening transactions, with eight credentials to store. Nothing in the proposal forbids that, and the spec states multi-merchant budgets and top-ups are out of scope for v1. But the economics show who this mode is for: a person typing, not an agent fanning out. The spec's title says "conversation", and that scoping is accurate.

Finding 5: the deadline decides who eats the cost of a late answer

Every reservation has a deadline: the merchant's recorded admission time plus the quoted maxTimeoutSeconds. Retries cannot extend it. At the deadline the merchant must either commit a durably established success or cancel the operation without charge. After cancellation, "every late worker/result" is permanently barred from committing a charge or voucher. The spec adds that finishing at the deadline "does not create a right to charge afterward", and that provider costs "do not alone authorize charging a failed resource request".

The example 402 in the spec uses 60 seconds. A merchant that quotes 60 seconds and runs a slow reasoning call eats the cost of every answer that lands at second 61. The remedy is to set the timeout from the handler's real tail latency, and to leave margin to commit before the advertised deadline, which the spec recommends. Our own current quote advertises 300.

The same section protects the buyer in a way worth copying. Time passing never releases a reservation by itself, and a canceled operation returns a fixed invalid_batch_settlement_svm_budget_operation_canceled reason rather than a pending loop. Retrying with the same requestId returns the retained outcome and never re-runs the work. A stored payment receipt cannot recreate an AI response, so the spec requires application-level idempotency as well.

The refund path, and where it stops being cooperative

The cooperative refund runs through the existing settle_and_seal and sealed distribute. The merchant injects its stored final operator voucher and the receiver authorizer's close authorization, so it never trusts a payer-supplied voucher. After the payer proof is accepted, the merchant keeps the close going without another prompt, even if the proof expires.

If the merchant is gone, the fallback is the payer's own onchain request_close, then the grace period and the seal, withdraw and distribute steps. The spec is blunt that this "does not promise an immediate or trustless refund" and "can allow final operator claims up to the deposit during grace". The example channel uses a withdrawal delay of 900 seconds. During those 15 minutes the operator can still claim.

The reopen rule is a good default. After a confirmed refund the default continuation is the remainder under the original expiry: with 1.75 USDC spent of 5, at most 3.25 USDC. A larger cap or a later expiry needs a fresh, explicit approval. Opening a new channel before the refund is confirmed is "a separate explicit funding decision, not automatic recovery".

What it means for LLM4Agents

Our gateway quotes an exact payment per call. A conversation budget is the opposite shape: pay once, then call many times on a credential. Agents already know that shape, because it is a prepaid API key. What the PR adds is a non-custodial version, where the balance sits in a channel the payer can close with a wallet signature and the merchant cannot raise.

The fit is partial. The spec targets browsers and humans: one wallet popup, one tab, sequential turns. Autonomous agents hit the single-operation-in-flight rule and the stranded remainder immediately. The mode is most relevant to a human-facing client of an agent product, such as a chat UI that sits in front of our gateway, not to the agent-to-gateway hop itself.

Three risks are worth naming. A credential in a header is a new secret class in our logging and tracing paths. A ceiling-based reservation exposes how loosely we derive per-call ceilings. And the deadline rule means the timeout we quote becomes a financial term, not just a transport one.

Staying on the frontier

  1. Do nothing in code yet. The PR is a draft with no reviews, no SDK and no wallet test. Watch it, and watch #3905 for the proof format it depends on.
  2. Audit our per-call ceilings. If we ever quote a ceiling, derive it from the model's real token cap and publish how. A flat floor is safe for exact, but it would be useless as a reservation.
  3. Set maxTimeoutSeconds from measured tail latency per route, and decide in advance who bears the cost of a result that arrives after the deadline.
  4. Add the credential to the redaction list now. Any header or field that could hold a spendable bearer secret should be scrubbed from logs, traces and model inputs, whatever scheme ends up using it.
  5. Prototype against a fork if the PR gains reviewers. Build the harness the acceptance matrix implies: concurrent reservations, a refund racing a stolen credential, and a retry after a lost response. Those three rows decide whether an implementation is safe.
  6. Pair it with buyer-side caps. A conversation budget bounds one merchant. The default caps in the SDK spend-controls audit still bound the agent across merchants.

Pay per call, without wallet popups

LLM4Agents gives autonomous agents an OpenAI-compatible gateway that bills in stablecoins over x402.

Register your agent