HTTP 402 is doing two jobs. The first unpaid call to a paid resource and a paid
call that came back without delivery both answer 402. The status code
carries no information about which one you're looking at, and a client that guesses
wrong does one of two expensive things: pays a second time for a request that is
still settling, or treats a rejected payment as "the seller is broken" and stops.
This is a client-side problem before it is a spec problem. Here is the decision order that works, and the part of it that depends on what the server sends.
A response can carry two documents that look similar and answer different questions:
PAYMENT-RESPONSE) describes the
payment event: did this attempt settle, and if not, why not.PAYMENT-REQUIRED) describes the
offer as it stands right now: what it costs, where to send it, what the current
reason is.They are allowed to disagree, and the disagreement is usually correct. A paid
attempt can fail with insufficient_funds while the challenge still
advertises a perfectly good price. Read the receipt to learn what happened to your
money; read the challenge to learn what you can buy next. A client that reads only
one of them, or that lets the newer challenge silently overwrite the retained
receipt, loses the only record of the attempt it already made.
Every retry decision reduces to one question: can I prove this response belongs to the attempt I am holding? Two carriers are usually available and they are not equal.
The client-supplied identifier is a hint. Any id you echo in a request can be copied by anyone who sees it, including by you on a second dispatch. It is fine for matching a response to a request within your own process. It is not evidence that a specific payment attempt exists.
The signed authorization is evidence. The bytes you signed — amount, destination, nonce, validity window — are the only part of the exchange the server cannot author for you. Everything else in the response is authored by the party you are trying to check.
Once you have both carriers, the cases separate cleanly:
| Case | Verdict for the client |
|---|---|
| A receipt for a different dispatch, or a locator carrying only an echoed client identifier | Neither proof nor conflict. Take the new challenge; do not record a payment. |
| A reference bound to this dispatch — matching the signed authorization, not just the route | Settled. Do not authorize again. |
| The same signed authorization presented twice | Already delivered. One payment, not two. |
The third row is the one that gets missed. Two dispatches that share a client identifier are two purchases that happen to look alike; one dispatch replayed is one purchase. The discriminator is the nonce inside the authorization, never the identifier you generated yourself. A server that delivers twice under one authorization has given away product, but a client that counts two payments there will reconcile a hole in its ledger that never existed.
Note also what "same route, same payer" does not mean: two successful payments a minute apart are a repeat purchase, not a duplicate. Scoping a conflict rule to the route alone rejects legitimate second buys.
If a challenge gains an optional machine-readable reason, three states exist and clients must keep them distinct:
Collapsing states 2 and 3 is what forces clients to reason about their own local state, which is exactly what a machine-readable reason was supposed to eliminate. The safe reading of an absent reason is unknown, and unknown means do not re-authorize on the strength of it.
A pending payment reference is a lookup hint, not a lock. The failure mode is architectural: a seller that dereferences it synchronously while building the 402 puts the facilitator's latency into the retry's time-to-first-byte, and a client that retries quickly can look like a flood of payment attempts when it is really one impatient loop.
Two rules keep this bounded:
Retry-After or equivalent) describes when the
offer is worth re-reading. It is not an authorization expiry, and a
client must not treat its expiry as permission to pay again.The pieces that are missing from the wire format today, in the order they cost clients money:
None of this requires a new transport. It requires deciding, once, which document answers which question — and writing that decision down where a client author will read it before they hit the retry loop.
minia2a is a pay-per-call market for x402 endpoints, priced in USDC on Base with five free trial calls per signed wallet. The paid-402 cases above come from running that gate in production and reading what clients did with ambiguous answers.
← All posts