Skip to content

Build1 publisher3 min readPublished

An x402 gate refuses a captured X-PAYMENT header only if it already wrote the key down

paygate402 hands signature recovery and settlement to the facilitator and keeps only a record of spent payments, so the whole replay defence of a machine-paid API is one write that has to land before the resource is served.

The Engineer · Build desk

Illustration accompanying An x402 gate refuses a captured X-PAYMENT header only if it already wrote the key down

What happened

  • An x402 paywall runs in three steps: a 402 answer carrying the terms the server accepts, the same request repeated with an X-PAYMENT header, and a verify-and-settle pass before the resource is served.
  • If the server does not remember which payments it has settled, a captured X-PAYMENT header stays good and buys the resource again on every replay.
  • The Go package paygate402 sits in front of an HTTP handler and leaves signature recovery, allowance checks and the transfer itself to the x402 facilitator.
  • Inside the gate, matching a presented payment against the accepted terms compares only the scheme and the network, because the rest sits in a payload the web layer cannot read.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Everything in the gate scales as ordinary stateless middleware except the spent-key store, so adding replicas to a paid endpoint means giving all of them one store they read before serving.
  • exposure Read access to proxy logs, traces or a pasted support ticket becomes spend access, and the people who hold that access are usually not the ones who reviewed the payment design.
  • decision Anyone building on this has to decide whether a valid signed quote counts as proof of payment; the write-up calls that conflation the most expensive mistake available in the design.
  • capability The length-prefixed encoding lets a server add quote fields in a later version without invalidating quotes already in flight, so the offer format can change without a flag day.

A signature keeps verifying. Verifying is its job, and it is why a second presentation of the same X-PAYMENT header succeeds unless the server stops it [8]. The header is a self-contained signed instruction sent over a stateless protocol, with nothing binding it to a session, a connection, or a single attempt [6]. Anything that reads it holds a complete payment: a proxy log, a retry loop, a shared trace, a browser extension, a support ticket with the captured request [7].

The package draws its boundary early. It does not check signatures and does not move money; in x402 the facilitator reads the scheme-specific payload, recovers the signature, checks the allowance, and submits the transfer [9]. The payload therefore stays opaque to the web layer, and the gate's matching compares only the scheme and the network [10]. Amount, asset and recipient are checked by the component that can read the payload [11]. The author wrote that "A middleware that decoded some base64 and called it verification would be worse than no middleware, because it would look like a paywall while charging nobody." [12]

The offer half genuinely needs no storage. A quote is the priced offer behind the 402: an amount, an asset, the moment the offer stops standing, and a nonce that makes it one of a kind [13]. It is signed with the server's own key, so a quote that comes back can be checked against what was actually offered [14]. The signature covers a canonical form instead of the JSON: a domain tag, then every set field in a fixed order, each part written as its length followed by its bytes [15]. Lengths instead of separators mean no value can be read as two fields, and moving the boundary between two concatenated strings is the classic forgery against that kind of signature [16]. A field left empty is not written at all, so a field added in a later version leaves the signed bytes of an older quote exactly as they were and its signature still verifies [17]. The expiry is signed as whole Unix seconds, because sub-second precision survives a JSON round trip badly. The two sides eventually disagree in the last digit, and that mismatch presents as a forged quote rather than a serialization bug [18].

The craft is real, and it covers the offer only. The write-up says a quote signature is the server's own, not a chain's, and that it asserts only "these were my terms". Treating a valid signed artifact as evidence of settlement, it says, is the single most expensive mistake available in this design [20].

In the example the accepted terms carry MaxTimeoutSeconds of 60 and the signer is built with TTL: time.Minute [5][19], so the offer window and the payment timeout in that configuration are both sixty seconds wide [22]. The write-up does not specify how long a spent key has to stay in the ledger [23]. Retention decides whether the store is a bounded cache with a minute or two of keys or a table that grows for the life of the endpoint. An operator needs that figure before picking a backend.

Two things have to be true for the stateless part of this design to hold in someone else's deployment. The facilitator has to be authoritative on amount, asset and recipient, since the gate compares scheme and network and nothing else [10]. Every replica behind the load balancer has to consult the same spent-key store, and check it before serving. If two replicas read different stores, a captured header spends once in each [3].

What to watch

  • A published retention window for spent keys would tell operators whether a bounded cache is enough or the ledger has to be a permanent table.
  • Whether the ledger interface makes the check-and-store step atomic, so two concurrent presentations of the same header cannot both pass.
  • Whether facilitators document what they actually verify, since the gate delegates amount, asset and recipient to them.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories