📅 Historical page. This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail. → See current

A 402 after payment is not a 402 before payment

November 15, 2026

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.

1. Precedence: receipt first, challenge second

A response can carry two documents that look similar and answer different questions:

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.

2. Which evidence is actually yours

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.

This is why "the receipt says it settled" is not the same as "I can prove I paid". A receipt is seller-authored. A client that reconciles only against receipts believes whoever answers the request.

3. Three retry cases that look identical

Once you have both carriers, the cases separate cleanly:

CaseVerdict 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.

4. A missing reason is not "no error"

If a challenge gains an optional machine-readable reason, three states exist and clients must keep them distinct:

  1. Reason present — branch on it.
  2. Reason absent, no attempt made — this is the first-contact offer. Nothing is wrong.
  3. Reason absent, attempt made — the server has no taxonomy information. That is not the same as "no error", and it is certainly not the same as "retry freely".

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.

5. The retry trap: don't dereference inside the 402 path

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:

6. What we would standardize

The pieces that are missing from the wire format today, in the order they cost clients money:

  1. An explicit first-contact reason. One member that means "you have not attempted payment yet", so its absence can never be read as a rejection.
  2. A precedence statement. Receipt describes the payment event; challenge describes the offer; when both are present they may disagree and neither erases the other.
  3. The evidence hook. Whatever identifier a rebate, refund or settlement decision is keyed on should be something the buyer can check against bytes it signed, not only against a document the seller wrote.

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