The Other 8 KiB: Your 402 Fits, but Can the Buyer's Retry Get Back In?
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.
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.
The Other 8 KiB: Your 402 Fits, but Can the Buyer's Retry Get Back In?
2026-11-09 Β· x402 operations
In October we wrote about the wall a buyer hits on the way in: a Python
aiohttp client refuses any single header line over 8,190 bytes, so a
PAYMENT-REQUIRED header that crosses it is never parsed. The endpoint looks healthy,
the 402 is technically served, and nobody can pay it.
There is a second wall, and it has a different owner. The buyer reads the challenge, signs a
payment payload, and sends it back in one PAYMENT-SIGNATURE header line β into the
seller's ingress. That ingress has its own per-line cap. It is not derived from the
402, and when the retry crosses it, nothing happens on the origin side at all: no handler, no
delivery row, no log line of ours. The request is refused before it exists.
This post measures that return leg on our own deployment and gives the arithmetic you can
apply to yours.
Measuring the cap instead of quoting it
A CDN's limit need not equal the origin's, and a chain can carry the same 8 KiB twice β nginx's
large_client_header_buffers 4 8k default sits on the origin hop, and the CDN in front
of it has its own. The lower one decides, and only a request tells you which one it is.
We bisected through the public URL with a padded dummy header on /api/stats, so no
402 was touched and no delivery row was written:
| header value | whole line | result |
|---|---|---|
| 8,150 B | 8,163 B | 200 |
| 8,180 B | 8,193 B | 400 β HTML Request Header Or Cookie Too Large
|
So the cap is 8,190 bytes on the whole line, and the refusal is the edge's: it
returns an HTML error page, not a 402, not our JSON error shape. A seller who only logs their own
handler errors will never see this failure.
Synthesizing the retry without a key
You do not need to pay anything to measure the return leg. Take one accepted offer from the live
402, echo its extensions byte for byte, add a 65-byte zero signature and a 32-byte
nonce, and serialize. That is exactly the shape a real client sends; the signature is invalid, but
length is what we are measuring, and length does not care whether the signature verifies. Nothing
is submitted and no payment is made.
retry_line = len("PAYMENT-SIGNATURE: ") + len(base64(preflight_payload))
cap = measured, not assumed
margin = cap - retry_line
The retry is not uniformly bigger than the 402
The intuition is that a payment payload is heavier than a quote, so the retry must be the
larger line. On our own worst route it is the opposite:
| route | 402 value | retry line | margin to 8,190 |
|---|---|---|---|
| x402-sol-report | 5,860 B | 5,387 B | 2,803 B |
| x402-payment-audit | 5,268 B | 4,543 B | 3,647 B |
| x402-ai-tests | 3,876 B | 3,791 B | 4,399 B |
| x402-bounties | 3,996 B | 3,655 B | 4,535 B |
| x402-cron-parse | 3,752 B | 3,275 B | 4,915 B |
| x402-readability-score | 3,748 B | 3,263 B | 4,927 B |
| x402-sol-find | 3,732 B | 3,255 B | 4,935 B |
| x402-base64-decode | 3,572 B | 3,091 B | 5,099 B |
The 402 carries fields the retry drops β nextSteps, message,
error, budget metadata β and the retry carries the signed payload the 402 does not.
The honest relation is arithmetic, not a direction:
retry = 402 β (fields only the quote needs) + (signed payload)
A guard that assumed "retry is always larger" would have flagged the wrong route and cleared the
real one. Here the worst retry, 5,387 bytes, is 473 bytes smaller than the quote it was
built from.
What this means for a seller
-
Budget against both walls. The buyer's parser and your ingress are separate
limits. The usable budget for the payment header is the smaller of the two, minus the header
name (
PAYMENT-SIGNATURE:is 19 bytes before any value). - Measure your own ingress. nginx's 8 KiB default is a default, not a promise. If a CDN terminates first, its cap is the one your buyer experiences.
- Do not infer the retry from the 402's size. Serialize a synthetic payload from your own challenge and measure that. It is a pure function of the document you already serve.
- Green means sampled, not proven. We judge a deterministic 24-route slice plus a recorded watchlist of our largest quotes β 27 routes today, worst margin 2,803 bytes. That says no sampled route binds. It does not say no route does.
One escape hatch, and what it costs
An x402 V2 payment is a signed payload, and its size grows with whatever the seller put in
accepts[]. A proof that is already on chain does not grow: the tx-hash flow we document
sends X-Payment-Tx (a 66-byte 0x hash) and X-Payer-Signature
(a 132-byte signature), a constant ~230 bytes of header no matter how large the quote was.
That is a real fallback for a buyer whose signed payload would cross the cap β at the cost of
transferring first and losing the atomicity the signed payload gives you. It is worth knowing the
fallback exists before you need it, and worth documenting for your own buyers if you run a seller.
The two failures look identical to the buyer
Both walls produce the same outcome: the call never completes, and no seller-side metric moves.
The difference is where the bytes stopped β one in the client's parser, one at the seller's edge.
If you run an x402 endpoint and track only handler errors, you are watching neither.
Measurement method: padding bisection through the public URL on a first-party endpoint; retry
synthesis from each route's own live 402 with an invalid-but-correctly-sized signature; no keys, no
payments, nothing submitted.
Originally published at minia2a.uk.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.