emin@budak: ~/blog — zsh

emin@budak ~/blog % cat http-402-payment-required.md

HTTP 402 Payment Required: The Status Code That Waited 29 Years

Every HTTP specification since 1997 has said the same thing about 402: reserved for future use. Agent payments finally gave it a job. What x402 puts on the wire, and what it changes for API design.

A switchboard of connection ports where one lit port with a coin slot links a client to a server.

In January 1997, RFC 2068 defined HTTP/1.1 and gave HTTP 402 Payment Required exactly one sentence: “This code is reserved for future use.” Twenty-five years later, RFC 9110, the current standard for HTTP semantics, says almost the same thing: “The 402 (Payment Required) status code is reserved for future use.”

For most of that time 402 was a curiosity, the one code in the 4xx block everyone had heard of and nobody could use properly. That changed when software agents started needing a way to pay for things over plain HTTP. This post looks at why 402 waited so long, what the x402 protocol actually puts on the wire, and what it changes for anyone who designs APIs.

Why HTTP 402 Payment Required never got a job

Mostly because the web solved payment above HTTP instead of inside it. Browsers got cookies, accounts and checkout pages. APIs got keys and monthly invoices. The payer was always a person who could sign up once and be billed later, so a status code meaning “pay now, then retry” had nothing to do.

There were serious attempts. In 2020 Lightning Labs introduced LSAT, later renamed L402. The server answers 402 with a WWW-Authenticate header carrying a macaroon and a Lightning invoice; the client pays the invoice and retries with the proof of payment. It works and has run in production for years, but it ties both sides to Bitcoin’s Lightning Network.

Meanwhile the code picked up improvised meanings. Stripe’s API, for example, documents 402 as “Request Failed: the parameters were valid but the request failed”, which usually means a declined card. Useful, but it reports that a payment failed rather than asking for one.

What x402 puts in the response

x402, started by Coinbase and now developed under the x402 Foundation, uses 402 for what the original authors seem to have had in mind. The flow in its specification:

Sequence diagram of a client, a server and a facilitator exchanging a request, a payment and a delivery.
  1. The client requests a resource.
  2. The server answers 402 Payment Required with a base64-encoded PaymentRequired object in a PAYMENT-REQUIRED header: what it accepts, on which network, how much and to whom.
  3. The client picks one of the offered options and builds a signed PaymentPayload.
  4. It retries the same request with that payload in a PAYMENT-SIGNATURE header.
  5. The server verifies the payload itself, or asks a facilitator through its /verify endpoint.
  6. If the payment is valid, the server does the work, then settles, directly on chain or through the facilitator’s /settle endpoint.
  7. The response is an ordinary 200 OK with the resource and a PAYMENT-RESPONSE header holding the settlement receipt.

The facilitator is the practical trick. The server never runs a blockchain node or thinks about gas; it forwards two JSON objects and gets a yes or a no. The protocol is chain-agnostic, with stablecoins such as USDC as the main use, and a scheme field decides how money moves: exact for a fixed price, upto for “charge what I used, up to this limit”, and batch-settlement for commitments that are redeemed later in bulk.

A minimal paid endpoint, sketched

Here is the shape of a paid endpoint. It is a sketch of the pattern, not the API of any SDK, and the point is how little of it is about payment.

// Sketch of the pattern; field names follow the spec loosely
app.post('/v1/summarize', async (req, res) => {
  const offer = {
    scheme: 'exact',
    network: NETWORK_ID,      // a network your facilitator supports
    asset: USDC_ADDRESS,
    amount: '20000',          // 0.02 USDC in base units
    payTo: TREASURY_ADDRESS,
    resource: '/v1/summarize',
  };

  const signed = req.get('PAYMENT-SIGNATURE');
  if (!signed) {
    res.set('PAYMENT-REQUIRED', b64({ accepts: [offer] }));
    res.set('Cache-Control', 'no-store');
    return res.status(402).json({ error: 'payment required', accepts: [offer] });
  }

  const check = await facilitator.verify(signed, offer);
  if (!check.valid) {
    return res.status(402).json({ error: check.reason, accepts: [offer] });
  }

  const result = await summarize(req.body);        // do the work first
  const receipt = await facilitator.settle(signed, offer);
  res.set('PAYMENT-RESPONSE', b64(receipt));
  return res.json(result);
});

Two details are easy to miss. The offer goes out in both the header and the body, so a program can parse the first and a person reading a failed request in a log can understand the second. And the order of operations is a business decision: settling before the work protects the provider, settling after protects the client. The x402 flow verifies first and settles after the work, which is the fair default for anything that can fail.

What a client has to do with a 402

The server side is the easy half. A client that meets a 402 has to make a decision, and an agent has to make it without asking anyone. This is the sequence I would build into any client that is allowed to spend money:

  1. Parse the offers, not the error text. Read the accepts list from the PAYMENT-REQUIRED header, check which scheme and network the client’s wallet supports, and discard the rest.
  2. Check the price against a policy before signing: a per-request limit, a per-session budget and an allow-list of payees. A 402 is an invoice from a stranger; nothing should sign it automatically without these three checks.
  3. Retry the same request with the same idempotency key. The paid retry has to be recognizable as the same request, so that a timeout followed by a second retry does not buy the resource twice.
  4. Verify the receipt. Decode the PAYMENT-RESPONSE header, confirm the amount and the payee match what was signed, and store the transaction reference next to the request ID.
  5. Stop on a second 402. If a paid retry comes back with another 402, something is wrong on one side. Retrying in a loop is how an agent drains a wallet on a bug.

None of these steps is exotic. They are the same controls a finance team applies to invoices, compressed into a few milliseconds and written as code. The difference is that nobody reviews them afterwards, so they have to be right before the first payment goes out.

What changes for API design

  • Prices become part of the protocol. Today a price lives on a pricing page. With 402 it lives in the response, per request, readable by a program. That lets an agent compare providers, and makes a quiet price change very visible.
  • Idempotency stops being optional. A client that times out after paying will retry. The same request key has to return the same result without a second charge. Payment APIs have used idempotency keys for years for exactly this reason.
  • 401, 403 and 402 finally mean different things. 401: I don’t know who you are. 403: I know, and you can’t. 402: you can, for a price. Clients can branch on the status code instead of parsing error strings.
  • Caching needs care. RFC 9110 does not list 402 among the heuristically cacheable codes, but the proxies and CDNs in front of an API have their own rules. Send Cache-Control: no-store on payment responses, and make sure a paid response is never served to the next caller.
  • Rate limits and prices interact. 429 says slow down; 402 says pay. An endpoint that can return both needs a clear order, or clients will end up paying to be throttled.

Where I would be careful

Paying per request moves risk around. The client now holds a wallet and a spending policy, so a bug in an agent’s loop can spend real money in minutes. A per-session ceiling, which upto and pre-funded escrows make natural, is the first control I would add. The server now has to treat its price as part of its API contract, with versioning and notice like any other breaking change. And both sides need logs that tie a request ID to a payment ID, because “why was I charged for this” is the first question anyone will ask.

After 29 years, 402 finally does something. The spec barely changed; what changed is who’s paying. When the caller is a program rather than a person, a price in the response is simply more practical than an account and a monthly invoice.