The usual way an x402 seller causes a double charge is not an attack. A buyer pays, the connection drops before the reply arrives, and the buyer's program asks for the same resource again. What the seller does with that second request is the whole story: it can show the receipt, hand over the goods to the right person, or send back a refusal that makes the buyer's client sign a brand new payment. This guide explains where those branches go wrong, why the failures are silent, and how to test the seller side for them. It covers the seller's code once a signed header arrives, not the storefront and not the buyer.

Why the repeat is a normal event

An x402 payment on the EVM networks is a signed authorization that the seller's facilitator submits on chain. The standard behind it, EIP-3009, gives each authorization a random 32-byte nonce that is unique to the signer, and the token contract can report the state of an authorization afterwards (EIP-3009). The x402 specification repeats the point: each authorization carries a unique 32-byte nonce, and the token contract itself refuses to accept a nonce twice (x402 spec v2).

That protects the chain. It does not protect the seller's own application logic. The chain refuses a second transfer for the same nonce, but the seller's server still has to decide what HTTP answer to give when the same header arrives again. The specification also allows the settle call to arrive repeatedly for one payment (x402 spec v2), so duplicates are expected inside the seller's flow, not just from the buyer.

An automated buyer does not complain. It retries, and if the answer says "sign again", a client that follows the usual flow may sign again. That is why a flaw here looks like a normal day in the logs.

Mistake one: refusing an expired header before reading its nonce

Each authorization has a window. EIP-3009 describes the two bounds as a way to make a transfer valid only after a time or only before a deadline (EIP-3009). In the x402 payload the upper bound is validBefore.

A common handler checks that bound first: if the deadline has passed, it answers with status 402 and a friendly line saying nothing was charged. That line is only true for a payment that never settled. Consider the buyer who paid, lost the reply, and retries an hour later with the identical header. The header is now expired, the handler answers with status 402, and a client that follows the usual flow may treat that status as an instruction to produce a fresh authorization (the sources do not describe client behaviour, so test this with the client your buyers use). The buyer then pays a second time for the same goods.

The order of checks fixes this. On any header, including an expired one, look the nonce up in the seller's own payment table before judging the deadline. If the row exists and is settled, pending or reserved, answer with status 409 and the stored receipt. Choose status 409 over status 402, because the latter is the one that invites a new signature. Only when the nonce is unknown does the old expiry refusal apply. How the buyer then receives the goods, for example through a lookup by receipt or a short delivery window, is the seller's own design and stays outside this guide.

Mistake two: handing over goods on a repeat without proof

When a nonce is seen again, some handlers simply deliver, reasoning that the buyer already paid. The trouble is that the settlement transaction is public. What the transaction carries on chain includes the signed fields themselves, so anyone watching the chain can rebuild a valid-looking header. EIP-3009 itself warns that an attacker watching the transaction pool can extract an authorization and front-run it (EIP-3009). A repeat path that delivers to any caller presenting a known nonce therefore delivers to strangers too.

A repeat should hand over the goods only against something a chain observer cannot produce. The signature inside a copied header is valid too, so it proves nothing by itself. Reasonable options, from general practice rather than from either source, are:

  • a fresh signature from the payer over a challenge that the seller issued with the first answer, checked against the payer address saved in the seller's own row;
  • a hash of the request body, saved at reservation time and compared on the repeat;
  • a secret handed to the buyer with the first answer.

If none of these matches, answer with status 409 and the receipt, and do not touch the goods. Any of these still needs a time limit, which is the next mistake.

Mistake three: measuring the repeat window with the buyer's clock

Suppose the repeat path does check the signer, but it allows repeats until validBefore plus some tolerance. That value was chosen by the buyer, and any observer can read it on chain. A header rebuilt from public data keeps working up to that deadline, and a buyer who picked a very distant deadline has made the window effectively endless.

The window should come from a timestamp the seller wrote itself, such as the moment the payment was reserved or settled. Keep it short and make it a seller-side constant. This is a design judgement from general practice, not a rule in the cited documents.

Mistake four: treating a pending settlement as a failure

The specification lists settlement_pending as a settle error reason, and describes it as a signal that the caller should look at the chain itself before choosing to try again (x402 spec v2). A settle response with a false success flag is therefore not always final. A handler that marks the row failed and answers with status 402 on every false flag will tell the buyer to pay again while the first transaction is still on its way to confirmation. If both confirm, the buyer has paid twice.

Keep the row pending, store the transaction hash if one is returned, answer with status 409 plus that hash, and reconcile later by reading the chain. Reserve status 402 for reasons that are terminal. The status choice is general practice, not wording from the specification.

Four requests to run against staging

Run these four requests against a staging copy of the seller, with a test token and a test wallet:

  1. Pay once and keep the full header. Replay it immediately. Expect the receipt or the goods for the verified payer, never a new payment demand.
  2. Replay the same header after its deadline. Expect the receipt with status 409. A demand to pay again here is the first mistake.
  3. Rebuild a header from the public transaction data, with a different request body, and send it. Expect a refusal and no goods.
  4. Make the facilitator return a pending reason in a test double. Expect a pending row and no payment demand.

Then break each rule on purpose, for example by moving the expiry check back above the nonce read, and confirm the matching request now fails. A test that stays green with the rule removed is not testing it.

When this does not apply

Scope is the seller side of the exact EVM authorization scheme described in the cited documents. It does not apply to other payment schemes, to sellers whose facilitator keeps all duplicate handling and returns the stored result itself, or to the buyer's own retry policy. The ordering advice, the choice of conflict status and the repeat proofs are general engineering practice, not statements from the specification, so check them against the client library your buyers use before relying on them. The specification does not describe idempotent request handling for sellers, which is why this guide leaves those details to the seller. This guide did not run any payments or load tests, and did not check any particular facilitator's behaviour beyond what the two sources say.

The reasoning above matches the habit described in our post on checking what a payment request asks for before paying, seen from the seller's side of the same exchange.

Reviewing the seller code

If you want a second reading of this code before launch, the x402-settlement-review skill reads payment-handling code you paste and reports the flaws in this guide plus a few neighbours. Every finding gets a short label, a location and a suggested change, and the answer ends in a single verdict. It also looks at attempt counters keyed on unverified wallet claims, public flags that raise what the seller pays for, and a health signal fed by the wrong facilitator. It reviews only the pasted code and stays silent on parts you did not include.

Read this post as Markdown: /blog/x402-settlement-replay-double-charge.md · Atom feed.