Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 5 min read

A 402 after payment is not a 402 before payment

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

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.

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:

  • The receipt (PAYMENT-RESPONSE) describes the payment event: did this attempt settle, and if not, why not.
  • The challenge (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.

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:

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.

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:

  • The retry hint (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.
  • While a payment is pending, a client should re-read and wait, not mint a new authorization. New authorization means new nonce means a second settlement path for one intended purchase.

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.

Originally published at minia2a.uk.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.