← Blog
August 31, 2026 · 16 min

One signature, fifty invocations: auditing x402's two-phase gap

x402 verifies a payment before the resource runs and settles it afterwards. Three independent research groups spent 2026 documenting what fits inside that window. We reproduced their attacks against the stack published four days ago.

The design is deliberate. Blockchain confirmation takes seconds; an HTTP request should not. So x402 splits payment processing in two: a read-only /verify before the handler executes, and a state-committing /settle after it returns. The protocol calls this the authorization payment flow, and it is the default for every scheme on every network.

Between those two calls, the chain has recorded nothing. The authorization is valid, unconsumed, and — because x402 carries it as a bearer artifact in a header — perfectly replayable. That interval is what the literature now calls the two-phase gap.

This post does three things: summarizes what the published attacks actually measured, reproduces them against @x402/express 2.24.0, and audits the monorepo at HEAD to separate what August's patches closed from what they only moved.

Three papers, one window

The most systematic treatment is Free-Riding the Agentic Web: A Systematic Security Analysis of x402 Payments (Ling, Huang, Du, Chen, Zhou, Wu and Wang; City University of Hong Kong, Zhejiang University and CUHK), posted 22 June 2026 and published at ATC '26. It distills five security invariants, assigns every finding to one of three layers — protocol, SDK, deployment — and names five flaw classes.

Two of them matter most here. F1, cross-resource substitution: the signed authorization commits to a payee and an amount but not to the resource being bought, so a signature minted for one endpoint can be re-attached to any equal-priced sibling. The authors report it working in 100 of 100 controlled rounds. Their exposure census — a full pagination of 24,875 resources in the CDP registry, 915 merchants across 868 hosts after de-duplication — found 331 of those 868 hosts (38%) exposing same-price sibling clusters, median size 3, maximum 178, with 91 hosts exposing ten or more mutually substitutable resources.

F2, the duplicate-settlement race: concurrent requests carrying the same authorization all clear verification before any of them reaches the chain. Against the official CDP facilitator on Base mainnet, using their own merchant so all economic impact stayed internal, fifty rounds of twenty concurrent requests produced duplicate service delivery in 6% of rounds — two distinct HTTP 200 responses carrying different payloads against a single confirmed settlement.

The second source, Five Attacks on x402 Agentic Payment Protocol (Li, Wang and Wang; Ohio State University, CSIRO and the University of Manchester, 12 May 2026), arrives at the same place from a different angle. Its replay finding reports 248 HTTP-layer grants from a single on-chain settlement against a live endpoint. Its cache-confusion finding is cleaner still: routed through nginx, 100.0% of unpaid requests over 1,000 trials were served cached paid content, dropping to 0.0% when the response carried Cache-Control: no-store.

The third is a black-box study: Exploiting the Two-Phase Gap in the x402 Protocol for Autonomous AI Payments (Hwang and Choi, Sungkyunkwan University, NDSS 2026 poster). They took seven public x402 services, acquired one legitimate payment proof from each, and replayed it in ten-way synchronized bursts. Five services suppressed it entirely — one HTTP 200 out of ten. Two did not: an on-chain data API returned four and then five successful responses in two bursts, and an x402 reference site returned between two and ten across four bursts. Their Table I carries the line that defines the whole problem: the number of transactions recorded on-chain was 1 in all bursts.

The invariant being violated — is not "one payment, one settlement". The chain enforces that on its own. It is "one payment, one grant". The token contract has no idea how many HTTP responses the merchant emitted.

What the specification says about it

We read the v2 specification at HEAD. Section 10.1, "Replay Attack Prevention", lists four protections: the 32-byte EIP-3009 nonce, contract-level prevention of nonce reuse, explicit validity windows, and signature verification over the authorization.

Every one of them is a chain-layer control. None of them requires the resource server to claim a payment before it grants service. That is not an oversight in the writing — it is a faithful description of what the protocol actually guarantees, and it is exactly the gap all three papers land on.

Section 6.1 is newer and more interesting. It now formalizes payment flows as an explicit table:

// specs/x402-specification-v2.md, section 6.1
authorization  verify → resource → settle → respond     // default
upfront        settle → resource → respond
escrow         settle → resource → settle → respond

with a stated invariant: "at least one check — a verify or settle before the resource — MUST run before the resource executes."

upfront and escrow are the two orderings that close the gap by construction. escrow in particular is the reserve-then-commit pattern the Free-Riding authors propose as pessimistic two-phase locking. Both are in the spec. The question is where they are actually available.

Reproduction: a twenty-way burst on the current stack

We built a harness against the packages published on 27 August 2026 — @x402/core, @x402/express and @x402/evm, all 2.24.0. A local facilitator implements /supported, a read-only /verify, and a /settle that models the chain: a 400 ms confirmation delay, and a nonce that can be consumed exactly once. A resource server exposes two equal-priced routes behind the stock paymentMiddleware. Each handler increments a counter, standing in for paid compute.

The client signs exactly one authorization for GET /weather using the real @x402/evm exact scheme, then fires N concurrent requests carrying the identical PAYMENT-SIGNATURE header.

// N = 20, one signed authorization, replayed concurrently
{
  "byStatus": { "200": 1, "402": 19 },
  "grants": 1,
  "handlerInvocations": 20,
  "facilitator": { "verify": 20, "settle": 20,
                    "settleOk": 1, "settleFail": 19 }
}

We ran it at N = 5, 20 and 50. The result is identical in shape every time: grants = 1, handler invocations = N. Twenty verifies, twenty settles, one nonce consumed, one HTTP 200.

The duplicate grant is closed. That is a real fix and worth stating plainly. The reason is visible in the middleware: @x402/express intercepts writeHead, write, end and flushHeaders, buffers every call, and replays them to the socket only after settlement returns success. Nothing reaches the client until the payment has committed. We checked the same property in the Hono, Fastify and Next adapters, in Go's net/http middleware — which wraps the writer in a responseCapture backed by a bytes.Buffer — and in the Python FastAPI and Flask middleware. All of them buffer, and all of them gate settlement on a response status below 400.

This is precisely the "deferred delivery" defense the Free-Riding paper proposes in its section 5.3, shipped by default across three language stacks. It also closes that paper's stream-interruption variant, where a client consumes the output and then kills the TCP connection before the settlement callback fires. You cannot interrupt a stream that has not started.

The cache attack is closed too. The 402 challenge carries Cache-Control: no-store — we confirmed it on the wire — and every paid response passes through withPrivateCacheControl, which appends private so shared caches will not store it. That is the exact mitigation the Five Attacks paper measured taking nginx leakage from 100.0% to zero.

What the fix converted the attack into

Now the other half of the measurement. Handler invocations equal N in every burst. The merchant executed the paid work twenty times, and fifty times, for one payment.

Nothing in the request path claims the authorization before the handler runs. Each concurrent request independently calls /verify, gets a clean answer, executes the handler, buffers the output, calls /settle, and only then discovers that another request consumed the nonce first. Nineteen of twenty responses are discarded after the compute that produced them was already spent.

For a static JSON endpoint that is a rounding error. For the workload x402 exists to monetize — inference, retrieval, tool calls — it is the whole cost. The v2 stack did not eliminate the duplicate-settlement race. It moved the damage from stolen output to stolen compute, which is exactly the failure mode the Free-Riding paper files under F4, denial of settlement, where they measured leakage ratios of 86.95% and 100% against a live inference deployment.

Two production reports of this class remain open in the repository: issue #1062, opened 31 January 2026, on a facilitator timeout shorter than Base's confirmation time, and issue #1805, opened 25 March 2026, reporting one settlement proof reused across five concurrent requests.

Cross-resource substitution still works, and now it is structural

Second reproduction. We signed one authorization against GET /weather and replayed it, unmodified, against the equal-priced GET /premium. One request, no concurrency, no header manipulation.

{ "target": "/weather", "replayedAgainst": "/premium",
  "byStatus": { "200": 1 }, "grants": 1 }

HTTP 200, the premium body, settlement succeeded. The reason is in the type definitions, and it is sharper in v2 than it was in v1.

The EIP-3009 message the client signs is fixed by the token contract: {from, to, value, validAfter, validBefore, nonce}. There is no field for a resource, and the merchant cannot add one — the typehash lives in the deployed ERC-20. So any binding has to happen at the SDK layer, over data the signature does not cover.

In v1, PaymentRequirements carried resource: z.string().url(), and the matcher chose to ignore it. That code is still in the repository at HEAD:

// typescript/packages/legacy/x402/src/shared/middleware.ts
return paymentRequirements.find(
  value => value.scheme === payment.scheme && value.network === payment.network,
);

In v2 the field is gone. PaymentRequirements is now {scheme, network, asset, amount, payTo, maxTimeoutSeconds, extra} — the resource moved out to a separate ResourceInfo, required on the PaymentRequired challenge and optional on the payment payload. The v2 matcher is genuinely stricter than v1's: it deep-equals every core field of the client's echoed requirement and subset-checks extra. But the resource is not among the fields it compares, because the object no longer has one. We grepped the server and HTTP paths for any comparison of an incoming payload's resource against the route being served. There is none.

So v2 made the predicate complete over a set of fields that excludes the only one that identifies what is being bought. Two routes that share a network, asset, price and payee are, at the protocol level, the same purchase. This is what the paper calls a floating authorization, and their registry census suggests it is not a corner case — a third of surveyed hosts expose sibling resources at identical prices.

What August actually shipped

Two merges on 25 August 2026 are directly relevant, and both are narrower than they first appear.

// Patch 1

PendingSettlementStore is recovery, not idempotency

PR #3214 adds a PendingSettlementStore interface in TypeScript, Go and Python, keyed by a deterministic identifier derived from the payment payload. The name suggests deduplication. The implementation is something else.

The store is written to only when a settle attempt broadcasts a transaction and then fails to confirm. On a retry, the mechanism reconciles against the already-broadcast hash instead of signing a second one. Entries are deleted on confirmation, and the in-memory default expires them after five minutes. On the happy path — verify clean, settle confirms — no entry is ever created. It cannot deduplicate a replay because it holds no record of a payment that worked. It is crash and timeout recovery for the facilitator, correctly scoped and clearly documented as such, sitting one layer below the problem.

The paired retry logic in the resource server is capped at exactly one attempt with no backoff, which is the right call: the mechanism layer owns any waiting.

// Patch 2

The upfront flow landed where it was least needed

PR #3240 adds the upfront payment flow — settle before the handler runs — to the exact scheme. We enumerated the declarations at HEAD. Thirteen asset-transfer-method rows across twelve mechanisms now advertise ["authorization", "upfront"], on EVM, SVM, Aptos, Stellar, XRPL, NEAR, Hedera, TVM, AVM, Keeta and Concordium.

Every one of them is exact: fixed price, known before the handler runs, no dynamic compute. The scheme where settle-before-deliver was already achievable by buffering.

The upto scheme on EVM — variable pricing, the one the papers measured on Arbitrum, the one that bills for tokens whose count is unknown at request time — declares { permit2: { supported: ["authorization"] } }. Authorization only. So does batch-settlement. The one place a pre-handler commitment exists for upto is the SVM channel variant, which declares ["escrow"]: a deposit settle before the handler, a claim or cancel settle after. That is the reserve-commit design the literature asks for, and it ships on exactly one network for exactly one scheme.

One more line from section 6.1 deserves attention. When a resource offers both authorization and a pre-handler flow for the same request, the spec says clients "SHOULD prefer authorization". From the buyer's side that is rational — do not pay before you have the goods. It also means the flow that protects the merchant is the one the specification tells every conforming agent to skip. Servers that want upfront will have to offer it alone, which turns a security posture into a negotiation the client can decline.

The honest scorecard

Against the published attack catalog, on the stack shipping today: duplicate grants under concurrent replay are closed by unconditional response buffering. Shared-cache leakage of paid content is closed by no-store on the challenge and private on the response. Stream interruption before settlement is closed for the same reason buffering closes everything else.

Still open: one authorization still buys N executions of the paid handler, because nothing claims the payment before the work starts. Cross-resource substitution succeeds on the first request against equal-priced siblings, and v2's type changes made it structural rather than incidental. The upto allowance path on EVM has no pre-deduction nonce and no pre-handler commitment. And the specification's own security section still describes replay protection as something the chain does.

None of this makes x402 unusable. It makes the deployment layer load-bearing in ways the protocol does not advertise, which is the same conclusion we reached auditing the default spend cap: the wire format is specified tightly, and the operational envelope around it is left to whoever deploys.

What it means for LLM4Agents

We are a merchant on this rail, and the workload we sell is the expensive kind. A gateway call is inference: GPU seconds spent before a single byte reaches the client. The distinction between "duplicate grant" and "duplicate work" that looks academic for a weather endpoint is, for us, the difference between a discarded response and a paid-for H100 minute we do not get back.

That reframes the burst result. On our stack, an attacker with one valid authorization cannot get fifty answers. They can get us to compute fifty answers. The revenue is protected; the cost is not. Any capacity planning that assumes requests-served equals payments-collected is wrong by whatever concurrency an adversary chooses.

Cross-resource substitution matters differently. Our routes are priced per model and per token, so equal-priced siblings exist by construction: two models at the same rate on the same network to the same payee are indistinguishable to the protocol. An authorization minted for a cheap route is a valid authorization for any route at that price. Access control on model selection cannot live in the payment layer, because the payment layer does not know which model was requested.

The parts that are already fixed matter too. Response buffering means a client cannot stream our output and then drop the connection before settlement — a real concern for long generations, and one we would otherwise have had to solve ourselves. The private cache directive means our paid responses will not be served from a CDN edge to someone who never paid, which for an OpenAI-compatible endpoint sitting behind any proxy is not a theoretical exposure.

The upto gap is the one that constrains our roadmap directly. Metered billing for token-counted inference is exactly what upto is for, and on EVM it ships with no per-deduction nonce and no pre-handler commitment. The scheme that fits our billing model is the scheme with the widest measured leakage.

Staying on the frontier

Concrete steps, in the order we think they should be taken.

First, claim the payment before the handler runs. One atomic insert on the tuple of authorization nonce and route, in whatever store already fronts the gateway, taken before any compute is dispatched. Losers get a 402 immediately. This is the single change that turns the fifty-invocation result into a one-invocation result, it costs one round trip to Redis, and it does not require anything from the protocol. Everything else on this list is secondary to it.

Second, bind the resource ourselves. The signature cannot commit to a route, but the server can refuse a payload whose optional resource does not match the request it arrived on, and can decline to advertise identical prices across routes with different access levels. Neither is protocol work. Both are a middleware policy we can ship this quarter.

Third, prefer commitment over authorization where the workload is expensive. For metered inference, escrow is the correct ordering: reserve the ceiling, generate, settle the actual. It exists in the spec and it ships for upto on SVM. Standing up an SVM-settled metered route is a working reference we can point at while pushing for an EVM equivalent — and the auth-capture escrow work already gives EVM most of the primitives.

Fourth, take the argument upstream. Two of the three concrete asks are specification-shaped: a resource commitment inside the signed payload, and a normative requirement that servers claim a payment exactly once before granting. The Free-Riding authors propose the first as extending the signed tuple with a hash of the HTTP request context. The second belongs in section 10.1, which today describes only what the chain guarantees. Both are worth a proposal to the working group, and the fact that upfront and escrow reached the spec inside a quarter suggests the door is open.

Fifth, instrument the gap. Emit the interval between verify and settle, and the count of settles rejected for an already-consumed nonce, as first-class metrics on every route. A rise in the second is a concurrent replay in progress, and it is measurable today. This slots directly into the GenAI semantic conventions work we already track — the span exists, it just needs the payment attributes attached.

The broader point is that x402's security is converging, and it is converging through measurement. Three groups published reproducible attacks; the reference stack shipped deferred delivery, cache isolation and two new payment flows in the months that followed. That is a functioning feedback loop. The gap is that the loop currently runs on academic timelines, and the deployments it protects are billing real money on Base today.

Pay per call, in stablecoins, over an OpenAI-compatible gateway

345+ models, x402 and EIP-3009 settlement, no subscription.

Register your agent