Skip to main content
In Stage B each model request authorizes a maximum and settles only the actual measured usage, via the x402 upto scheme.

The scheme: authorize max, settle actual

A stock x402 request carries no payment on its first attempt. The gateway answers 402 with a body-aware maximum quote: the request is parsed into an input-token upper bound and an output-token cap, priced against the sealed catalog price, and the resulting maximum_amount is the single ceiling the authorization binds. The payer signs a Permit2 PermitWitnessTransferFrom for that maximum. After the streamed response completes, the gateway settles the actual priced usage, never more than the maximum. actual <= maximum is enforced three times: in the gateway client, in the adapter’s word-by-word calldata decode, and on-chain by the Permit2 authorization itself. The witness {to, facilitator, validAfter} is what makes maximum-authorize safe: the facilitator can settle any amount up to permitted.amount, to the treasury in the witness and nowhere else.

Why Permit2-only, and why three contracts went to Sepolia

In current x402-rs, upto is implemented with Permit2 and a hardcoded UptoPermit2Proxy. ERC-3009 cannot substitute: its signed value is fixed at signing time, so it serves the fixed-price exact flow (Stage A) and nothing more. Stage B therefore requires the canonical Permit2 contracts to exist on the chain: Permit2 itself is an OP Stack preinstall at 0x000000000022D473030F116dDEE9F6B43aC78BA3; its observed runtime hash equals the repository’s source-locked build, closing the canonical-source comparison gate.
Provenance honesty: both facilitator prerequisites reproduce their canonical CREATE2 addresses exactly, so the deployed code is cryptographically bound to its address. But the Exact proxy’s init code comes from x402-foundation’s frozen artifact (a fresh forge build embeds different CBOR metadata), and the ERC-6492 validator is verified by Sourcify match rather than a git-revision pin. Neither rebuilds as cleanly from source as the Upto proxy does.

The request binding

Every immutable request and chain fact is captured once in an X402RequestBinding (project, API key, protocol, route, request ID, fingerprint, maximum, expiry, network, asset, treasury, facilitator, Upto proxy, and transfer-method metadata). The binding produces:
  • a binding_digest: length-prefixed SHA-256 over every field, under the domain GIWA/x402/request-binding/v1;
  • the signed Permit2 nonce: derive_upto_authorization_nonce(binding_digest), under GIWA/x402/upto-nonce/v2.
Because Permit2 signs that nonce, a valid signature cannot be rewrapped for a different project, API key, route, request, price maximum, expiry, or chain configuration. Quote expiry is issued once at challenge time and carried back unchanged inside the signed header; a stale or freshly extended expiry is rejected with a new 402, never silently repriced.

The settlement journal: BoundV2 canonical, LegacyV1 frozen

The isolated x402 adapter, which links no x402 crate into the gateway, owns the sole settlement side effect, fenced by an append-only journal:
  1. Before broadcasting, settle writes a prepared journal record keyed by the payment’s JournalIdentity, recording a scan_next_block cursor read before the side effect.
  2. If a prepared record already exists without a transaction hash, the adapter refuses to broadcast again and returns FacilitatorUnavailable. Recovery is read-only.
  3. Recovery scans blocks from the cursor for a settlement transaction from the facilitator to the Upto proxy whose decoded calldata matches the authorization. More than one match is a hard error.
Double-settle prevention is three independent layers: the Permit2 nonce bitmap on-chain, the journal fence in the adapter, and the gateway’s idempotent accept plus lease- and hash-fenced finalize. The adapter journal speaks two formats: BoundV2 (binding-keyed) is canonical for all new requests; LegacyV1 (GIWA/x402/upto-nonce/v1, flat request shape) is frozen and exists only so durable rows and journals written before the binding existed can still recover. New execution never falls back to it.

Response-loss recovery

The fault harness injects a lost settle response between the adapter and the facilitator on the local localnet topology, then proves every journey reaches exactly one terminal settled state with a recovery proof digest:
  • pre-deadline: the response is lost inside the authorization window; one /recover re-read finalizes the payment;
  • post-deadline: the response is lost across the recovery delay; the journal scan path finds the mined settlement;
  • native_v2: the full native gateway flow under the same fault, including database evidence that exactly one admission, payment, ledger row, and receipt snapshot exist.
A client disconnect mid-stream is handled separately: settlement commits in a detached background task, the success suffix is emitted only after commit, and the terminal receipt stays retrievable at GET /v1/receipts/:request_id regardless of the stream’s fate.

Next steps