← Blog
August 12, 2026 · 15 min

auth-capture: x402's refund scheme, measured against the chain

Three x402 schemes move money forward. The fourth one is the only one that can move it back. We audited it against the repo and against the chain, and the two tell different stories.

x402 now has four payment schemes. exact transfers a fixed amount. upto authorizes a ceiling and settles real usage. batch-settlement stores a commitment and redeems it later. All three are push payments: once settlement lands, the money is gone and the only recourse is the seller voluntarily sending a new transfer.

auth-capture is the fourth. It is the only scheme in the protocol where the payer can get funds back without the seller's cooperation. That makes it the most interesting scheme for autonomous agents — an agent that pays for a job it cannot pre-verify needs an exit — and the least finished one in the repository.

We cloned x402-foundation/x402 on 2026-08-12 at HEAD c8247c4c, read both spec documents and every line of the shipped implementation, pulled the escrow contract source from upstream, and then swept 24 hours of Base mainnet logs to see what the scheme's foundation is actually doing. This continues the audit series that covered the extensions layer, the facilitator API, and the self-hosted settlement plane.

What the scheme actually is

auth-capture is not new cryptography. It is an HTTP binding over an existing, audited contract stack: base/commerce-payments, MIT-licensed, created 2025-03-06, currently 134 stars, with a single tagged release — v1.0.0, published 2025-05-07 — deployed to Base mainnet and Base Sepolia.

The stack is a singleton escrow, AuthCaptureEscrow at 0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff, plus a set of token collectors that each know how to pull funds under a different authorization primitive. Upstream deploys six collectors. x402's scheme wires two of them: the ERC-3009 collector at 0x0E3dF9510de65469C4518D7843919c0b8C7A7757 and the Permit2 collector at 0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26. The other four — including the Spend Permission collector that would connect this to the session-key world and the pre-approval collector — are left on the table.

The escrow has been reviewed five times by Coinbase Protocol Security and Spearbit between March and April 2025, with a sixth Spearbit report dated 2026-07-22 covering a later change. It is a plain contract — ReentrancyGuardTransient, an immutable token-store implementation, no proxy, no upgrade path. On Base mainnet today it holds 11,053 bytes of code.

Two paths, five verbs, three deadlines

The scheme selects between two settlement paths with a single boolean, extra.autoCapture.

With autoCapture: false — the default — the facilitator calls authorize(). Funds leave the payer and sit in escrow. The server delivers the resource. Later, an entity called the captureAuthorizer either calls capture() to finalize funds to the receiver, or void() to release them back. If the captureAuthorizer does nothing before the capture deadline, the payer calls reclaim() and takes the money back unilaterally. After a capture, refund() remains available until a second deadline.

With autoCapture: true, the facilitator calls charge(). Funds go straight to the receiver. No escrow, no void, no reclaim — only refund() within the refund window.

Three absolute timestamps govern the lifecycle, and the contract enforces their ordering:

// AuthCaptureEscrow, on every authorize() / charge()
if (preApprovalExp > authorizationExp || authorizationExp > refundExp) {
    revert InvalidExpiries(preApprovalExp, authorizationExp, refundExp);
}

preApprovalExpiry is derived client-side as now + maxTimeoutSeconds and blocks settlement after it passes. authorizationExpiry is the wire field captureDeadline: it blocks capture and enables reclaim. refundExpiry is the wire field refundDeadline. The practical consequence is worth stating plainly: for the receiver, finality is not capture, it is the refund deadline.

The captureAuthorizer is the whole trust model — it is the only address the escrow will accept as msg.sender for authorize, capture, void, refund and charge. The spec says it may be the facilitator's EOA or any smart contract that ends up calling the escrow. It is set by the server, in extra, and the payer signs over it.

What the client signs

The payer produces exactly one signature. Everything else is reconstructed by the facilitator.

The clever part is the nonce. Rather than carry the payment parameters in a witness struct — the pattern upto uses with Permit2 permitWitnessTransferFromauth-capture derives the nonce from the payment itself:

paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer))
nonce           = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash))

The payer field is zeroed so the facilitator can recompute the hash before it knows who is paying. Every other field of the on-chain struct — receiver, token, max amount, all three expiries, both fee bounds, the fee receiver, the captureAuthorizer — is inside that hash. Tampering with any of them changes the nonce, and the nonce is what was signed. Freshness comes from a client-generated 32-byte salt, which is also in the struct.

That gives the scheme a genuinely elegant property: a single field check on the wire nonce transitively enforces equality on twelve on-chain fields. The spec's verification list makes this explicit at step 12, and correctly notes that field-by-field checks become unnecessary.

The signature itself is either an ERC-3009 ReceiveWithAuthorization — the same gasless primitive that sits under exact on EVM, with the EIP-712 domain bound to the token contract — or a Permit2 PermitTransferFrom with no witness at all. EIP-6492 wrapping is supported for smart wallets that have not been deployed yet.

Finding one: the scheme ships a client and nothing else

The specification describes a 13-step verification procedure and a 7-step settlement procedure for the facilitator, complete with a table mapping seventeen typed contract reverts to stable invalidReason codes. None of it exists in code.

In @x402/evm — version 2.22.0, published 2026-08-11, 487,567 downloads in the month to 2026-08-09 — the other three schemes each ship client/, facilitator/ and server/ directories. auth-capture ships client/ and nothing else. The package exports exactly one subpath for it, ./auth-capture/client. There is no server middleware, no facilitator, and no server example in the repository; the client example's README says to "point RESOURCE_SERVER_URL at any auth-capture endpoint," which presupposes one exists.

Across the other language SDKs the count is zero. Python, Go and Java contain no occurrence of the string auth-capture in any form. Our earlier audit of x402-rs found seven schemes implemented; auth-capture was not among them.

The package README is honest about it: "This package currently ships the client only: detecting auth-capture payment requirements and signing the payment payload. Server and facilitator support follow in a later change." That was written for PR #2486, merged 2026-05-29. It is the last commit that touched auth-capture anywhere in the repository. The spec was merged 2026-05-12 and renamed 2026-05-20. In the two and a half months since, the repo has moved on and the scheme has not.

Two PRs are still open from that period: #2308, the original TypeScript SDK proposal from 2026-05-14 with eleven comments, and #2359, a spec update from 2026-05-18 with six comments that would replace the autoCapture boolean with an explicit payload.type covering authorize, charge, capture, void and refund as server-requested operations. Neither has landed.

A smaller symptom of the same neglect: the package README and the client example README both link to scheme_auth-capture_evm.md. The 2026-05-20 rename made the real filename scheme_auth_capture_evm.md. The hyphenated URL returns 404 on GitHub today; the underscored one returns 200. Every pointer from the code to the spec is broken.

Finding two: the fee semantics have already drifted

This one matters more, because it is a live divergence between the spec and the contract it names as its source of truth.

The x402 spec documents the fee system as basis points, applied on-chain:

Fee distribution: feeAmount = amount * feeBps / 10000, remainder goes to receiver.

Its typed-revert table maps the contract error FeeBpsOutOfRange to the reason fee_bps_out_of_range.

Upstream, commerce-payments PR #90 — opened 2026-06-22, merged 2026-07-16, titled "Rounding and billing fix" — replaced that entirely. On main, capture() and charge() take an absolute uint256 feeAmount instead of a uint16 feeBps. The payer-signed minFeeBps and maxFeeBps survive, but they now derive bounds that the operator-supplied absolute amount must fall within:

minFee = amount * minFeeBps / 10_000
maxFee = amount * maxFeeBps / 10_000
require(minFee <= feeAmount <= maxFee)

The error FeeBpsOutOfRange was renamed to FeeAmountOutOfRange, and the PaymentCharged and PaymentCaptured events changed field types. The PR describes itself, in its own words, as a "Breaking public ABI change" and tells integrators to migrate before upgrading. Spearbit audited it on 2026-07-22.

So the x402 spec now documents fee arithmetic the upstream contract no longer performs, and instructs facilitators to decode an error that no longer exists. The saving grace is that the deployed contract has not moved: we confirmed from the event topic hashes in our on-chain sweep that Base mainnet is still emitting the v1.0.0 signatures with uint16 feeBps. There is no v1.1.0 release and no new deployment address.

That reprieve is temporary, and the redeploy will be sharper than it looks. AuthCaptureEscrow is not upgradeable, so shipping the new ABI means a new address — and the escrow address is hashed into the nonce derivation. A redeploy silently invalidates every signature shape in the scheme, not just the fee call.

Finding three: the client checks presence, not policy

Reading AuthCaptureEvmScheme.createPaymentPayload line by line, the validation is entirely structural. It throws if name, version, captureAuthorizer, feeRecipient, captureDeadline, refundDeadline, minFeeBps, maxFeeBps or maxTimeoutSeconds is missing or of the wrong type. It then signs.

It never compares them to each other, and it never compares them to anything the payer would care about.

It computes preApprovalExpiry = now + maxTimeoutSeconds and never checks that the result is less than or equal to captureDeadline. A server advertising a generous maxTimeoutSeconds and a near-term captureDeadline gets a valid signature that is guaranteed to revert with InvalidExpiries — wasted round-trip, and a failure mode invisible until settlement.

More consequentially, there is no ceiling on captureDeadline itself. Reclaim is only available after that timestamp. A server that sets it a year out gets an agent's funds locked in escrow for a year, with capture or void entirely at its discretion for the whole window, and the client SDK raises nothing. Nor is there any allowlist on captureAuthorizer, the single address that controls the money once it is in escrow.

And feeRecipient has a trap that issue #3004, opened 2026-07-31 and still without a single comment, describes precisely: setting it to address(0) does not mean "no fee recipient." It means the captureAuthorizer may name any non-zero address at capture time. It is a wildcard payout authorization, bounded only by maxFeeBps, and a wallet UI that renders it as 0x0 shows the payer something that is not what they signed.

Which brings us to the gap underneath all of these. specs/CONTRIBUTING.md asks scheme authors to document replay prevention, authorization scope and settlement atomicity under a security section. Seven such sections exist across the network documents of exact, upto and batch-settlement. In the two auth-capture documents, the word "security" does not appear once — not as a heading, not in prose. PR #2902, which would add one covering exactly these operational races and the captureAuthorizer trust model, has been open since 2026-07-18.

What the chain says

Specs describe intent. We wanted usage, so we ran two sweeps on 2026-08-12.

First, the demand side. We sampled 1,500 of the 15,299 resources indexed in the x402 Bazaar and counted schemes across every accepts entry: 3,152 exact, 110 upto, 31 batch-settlement, 2 onchain, 1 agent-pay. Zero auth-capture. Nobody is advertising a refundable x402 endpoint, which follows directly from the fact that no server or facilitator code exists to serve one.

Second, the contract underneath. We pulled every AuthCaptureEscrow log on Base mainnet across 43,000 blocks — from 2026-08-11T09:12:49Z to 2026-08-12T09:06:09Z, a clean 24-hour window — and decoded them.

// 24 hours of AuthCaptureEscrow on Base mainnet

2,137 authorize · 2,131 capture · 18 charge · 4 void · 2 refund · 0 reclaim

The contract is busy: roughly one payment every 40 seconds, $158,515.61 in total authorized volume, with amounts running from $0.01 to $10,500 and a median of $10.50. Every single payment was USDC, and every single one used the ERC-3009 collector. The Permit2 collector saw no traffic at all.

The two-phase path dominates completely — 18 of 2,155 payments, under 1%, took the charge() shortcut. And the recourse machinery, the entire reason this scheme exists, fired six times: four voids and two refunds, 0.28% of payments. Reclaim fired zero times, which is the healthier reading of the same number: no captureAuthorizer left a payer stranded past the deadline in this window.

Concentration is the other finding. Four distinct captureAuthorizers appear in the window, and one of them accounts for 2,103 of 2,155 authorizations — 97.6%. The top two are not EOAs; both are ERC-1967 proxies with byte-identical bytecode, pointing at two different implementation addresses. The address a payer signs into PaymentInfo.operator, and which then holds unilateral capture and void rights over their escrowed funds, is a contract whose logic can be replaced after the signature is made.

Put the two sweeps together and the picture is clear. The escrow is real, live, audited and moving six figures a day — driven by Coinbase's commerce products, not by x402. The x402 binding on top of it is a spec, a client, and no counterparty.

What it means for LLM4Agents

The gateway already runs this state machine. Our reserve → proxy → settle cycle is authorize-then-capture with the hold kept off-chain in our ledger: we reserve against a balance before the inference call, proxy the request, then settle the real cost. auth-capture is the same shape with the hold moved on-chain and the release right handed to a named third party.

For per-call inference, that trade is bad and the data says so. Escrow adds an on-chain hold, a second transaction to capture, and a liveness dependency on a captureAuthorizer — in exchange for a recourse path that fires on 0.28% of payments. For a $0.004 model call, the settlement overhead exceeds the disputed value by orders of magnitude. exact remains correct for the gateway's core billing, and upto remains the right primitive when the true cost is unknown at request time.

Where it stops being bad is at the top of our price range. The gateway does not only sell tokens. It sells long-running jobs against the Workspace — video renders, deep research runs, batch document work — where a single request can cost dollars, take minutes, and produce output the buyer cannot verify before paying. That is exactly the shape auth-capture was designed for, and exactly where an agent buying from us has a legitimate reason to want an exit.

The threat is subtler than a competing scheme. Every agent-payments framework we have audited this quarter is converging on the same primitive under different names — the constraint set of Verifiable Intent, the allowance of the Agentic Commerce Protocol, the spend permission of ERC-7715, the upto ceiling. auth-capture adds the piece all of those lack: a reversal. If a card-rail protocol ships credible agent-side dispute resolution before the stablecoin rails ship a working escrow scheme, "final settlement" stops reading as a feature and starts reading as a missing one.

The opportunity is the inverse of finding one. A specified, audited, contract-backed scheme with a complete verification procedure and no facilitator implementing it is an open lane. Whoever ships the first working auth-capture facilitator defines how refundable x402 works in practice.

Staying on the frontier

Six steps, in order of leverage.

Wrap the client in a policy guard before we ever pay under this scheme. The SDK validates types; we need to validate values. Reject captureDeadline beyond a configured horizon. Enforce preApprovalExpiry <= captureDeadline locally so we never sign a payload that is guaranteed to revert. Refuse feeRecipient == address(0) unless explicitly allowlisted for that host, and refuse maxFeeBps above a ceiling. Allowlist known captureAuthorizers. This is a small wrapper around AuthCaptureEvmScheme and it belongs upstream as a PR, not in our fork.

Build the missing facilitator against Base Sepolia. The spec hands over a complete design — thirteen verification steps, seven settlement steps, seventeen typed reverts. Pin it explicitly to commerce-payments v1.0.0 and put the fee ABI behind a version constant so PR #90's redeploy is a config change, not a rewrite. Run it in staging under the same conformance gate we use for the rest of the settlement plane.

Expose auth-capture only above a cost threshold. Map authorize → deliver → capture onto our existing reserve → proxy → settle and offer it on jobs where the reserve exceeds a dollar figure worth disputing. Keep exact everywhere else. Publishing the threshold is part of the product.

Upstream the three findings. The broken spec links are a two-line fix. The fee-semantics drift is a real correctness issue that will bite the first facilitator to ship. The missing security section already has an open PR — #2902 — that deserves the operational races we found attached to it, including the client-side ordering gap and the feeRecipient wildcard from #3004.

Track issue #3065 closely. The verification-gated release proposal — co-signed acceptance criteria that decide capture versus void mechanically instead of by the captureAuthorizer's judgment — is where agent-to-agent job payment actually goes. It has sixteen comments in under a week, which is more movement than the scheme itself has seen since May. Coupled with the offer-receipt extension it would produce the anchored proof-of-payment that the ERC-8004 reputation layer is missing.

Alert on the redeploy. Because the escrow address is an input to the nonce hash, a new deployment changes every signature in the scheme, not just the fee call. Pin the canonical addresses in config, probe them on boot, and page on drift.

The scheme is unfinished, and that is the point. Three months of spec with no counterparty is not a dead end — it is an unclaimed position in the only part of the x402 surface where money can still move backwards.

Pay per call. Settle in stablecoins.

An OpenAI-compatible gateway with x402 settlement, no prepaid credit, and no lock-in.

Register your agent