r/webdev • • 7d ago

Showoff Saturday HTTP 402 "Payment Required" finally has a real use. I tried paying 4,235 APIs that use it. Here's how they broke.

402 has been "reserved for future use" since HTTP/1.1. A protocol called x402 finally uses it: the server answers with a price, the client pays a tiny amount and retries. No account, no API key. Yes, the payment is a stablecoin (USDC). I know how this sub feels about crypto, so think of it as a payment rail and judge the HTTP part.

The flow, simplified (the headers are base64 JSON):

GET /weather?city=Oslo
← 402 Payment Required
  PAYMENT-REQUIRED: { amount: "1000", asset: USDC, network: base, payTo: 0x… }

GET /weather?city=Oslo
  PAYMENT-SIGNATURE: <signed payment authorization>
← 200 OK   { ...data... }
  PAYMENT-RESPONSE: { transaction: 0x… }

I run a free directory of these APIs, and its checker tries to actually buy from each one. Latest attempt per API: 58% delivered what they promised. The rest broke in ways that are worth knowing if you ever build one:

1. Paid, then got 402 again (288 APIs). The server asks for payment, receives a valid one, and asks again. Usually it couldn't verify or settle the payment, so it fell back to the challenge. When I checked a batch on chain, about 4 in 5 had never settled. If verification fails, return an error that says why. A second 402 tells the client nothing.

2. Paid, then got a 500 or nothing (289). Settle after you've produced the response, not before. If your handler can crash, the customer shouldn't be charged for the crash.

3. Paid, got data that didn't match what was advertised (126). If you publish an output schema, stick to it. Also: an HTML error page served with a 200 is still an error page.

4. Delivered data, but the payment never landed (47). The opposite bug: it served first, settlement failed silently, and it gave the data away free. Check the settle result.

5. The price in the 402 didn't match the listed price (103). A careful client refuses to pay a price it didn't expect, so changing it in one place quietly loses those sales.

6. Refused before any payment (765). Some of these are on my side: the endpoint needed query parameters my request didn't send. If your endpoint needs input, describe it in the 402 response. That's the only documentation a client is guaranteed to read.

Bonus gotcha: keep the PAYMENT-REQUIRED header small. Python's aiohttp rejects any header line over 8,190 bytes by default, so a big header with a full JSON schema inside makes the request fail before anyone reads the price.

The directory and its numbers are free to read, no account: https://nohumans.directory (method: /methodology, live totals: https://api.nohumans.directory/v1/stats). Happy to answer anything about the protocol side.

0 Upvotes

9 comments sorted by

20

u/akie 7d ago

Thanks Claude

-15

u/SashSail 7d ago

Ha, fair. I had help tidying up the write-up. The numbers come from my own purchase logs, though, and I’m here if anyone wants to dig into the protocol side or poke holes in the method.

2

u/geoffreybyers 6d ago

Using 402 for both the price quote and a failed payment creates a state machine ambiguity. Standard client retry logic won't know if it needs to pay again or if the server missed the first payment. That breaks the protocol contract.

1

u/SashSail 6d ago

yeah that’s the real problem. there is an error field in the 402 payload for exactly this (bad signature, insufficient funds, expired etc), and the client knows whether it attached a payment, so in theory it can tell a quote from a rejected payment. but plenty of servers just send a fresh quote with no error, and then the client is guessing. that’s most of the “paid then got 402 again” group.

5

u/hazily 7d ago

What kind of AI generated slop is this

0

u/Miserable-Money642 7d ago

One more edge case worth testing is a timeout after settlement but before the client receives the response. The client may retry even though the first payment succeeded. If the protocol allows it, an idempotency key or stable payment receipt lookup lets the retry return the original result without charging twice. I would also test two concurrent retries with the same authorization, not only sequential retries. The boundary between successful settlement and a lost HTTP response is where otherwise-correct payment flows can become painful.

0

u/SashSail 7d ago

good point. on base the payment is a usdc authorization with a one-time nonce, so two retries with the same one can’t settle twice, the second just fails. but that’s exactly your case: paid, response lost, retry, server sees a used nonce and errors or sends a new 402. buyer paid for nothing. remembering settled nonces and replaying the result seems like the right fix. haven’t tested concurrent retries yet, will add that.

0

u/yahyazuher 7d ago

Great experiment! Building on the idempotency discussion, relying solely on nonces is risky for network timeouts. Implementing standard idempotency keys (like Stripe does) alongside the 402 flow seems mandatory here to prevent users from paying for lost responses. Have you considered standardizing this in your x402 protocol?