402 Payment Required and x402
You often find out about a brand new HTTP status code out of nowhere. HTTP 418 I'm a teapot is one such famous example that surprises people. The HTTP 402 Payment Required is another that might not be encountered in the wilderness too often.
While I did know about it for a long time (not nearly as long as it has existed, tho, since 1997!), only relatively recently did I stumble upon x402, and decided I wanted to see how exactly it works. It seems to have had a drastic increase in trend/hype/exposure recently, likely due to association with AI and automatic payments, so why not check out what all the fuss is about!
For the 402 status code, its usage can nowdays be seen e.g. in cases when the server responds to a failed transfer, a transfer might be failing and the server might be doing something akin to this:
POST /merchant/transfers/payment HTTP/1.1
Host: payments.example.com
Content-Type: application/json
Content-Length: 999
{
"payment_transfer": {
payment_reference: "FOO",
iban: "BAR",
...
}
}
This usually happens while interfacing with another service, rather than with an end user!
x402 has a slightly different motivation behind it. It takes the status itself at face value - payment failed? Okay, why not actually pay for it to turn 200 =).
The idea is that you can paywall any API endpoint (or resource) and have the client handle payment automatically, without redirects, without checkout pages, without any of the usual drag. It could be paywalling a streaming endpoint, because why not? The server says "this costs X" and the client signs a payment, and the request goes through. All within a single HTTP exchange.
Usually crypto tries to find problems that it solves. I'm not sure if this is such a case, it might be, it might not. But it's certainly interestnig how easy it is to implement. x402 relies on USDC stablecoins and ERC-3009 (transferWithAuthorization), which lets you sign a transfer without needing to submit it yourself. A facilitator sits between the client and the server, verifying and settling the payment on-chain. You can also do it without the facilitator (it might even look simpler, e.g. you just interface with the blockchain via Ankr or such, and look up a transcation hash to verify the user paid for it, but this keeps it a lot "dirtier" both for the end user as well as for you). But if you do use a facilitator, it acts like the middleman that makes sure nobody gets scammed in the process.
It looks something like this with how I implemented it
┌─────────────────────────┐
│ Client Request │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐ ┌──────────────────────────────────┐
│ Payment header found? │─NO─▶ Return 402 + PAYMENT-REQUIRED │
└────────────┬────────────┘ └──────────────────────────────────┘
YES
│
▼
┌─────────────────────────────────────────────┐
│ User signs USDC transferWithAuthorization │
│ (ERC-3009) │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Client resends with PAYMENT-SIGNATURE │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Facilitator verifies signature │
│ Facilitator submits tx to Polygon │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Payment confirmed │
│ Resource returned with settlement headers │
└─────────────────────────────────────────────┘
My example works on Cloudflare Workers and is hosted here at x402.matija.eu and it only required a couple of things: a server that knows how to return 402 responses with the right headers (well, the CF worker in this case), a client that can intercept those responses and produce a signed payment (can be an extension, or an app browsing the page), and a facilitator that handles the on-chain settlement (payai.network in this case).
On the server side, you wrap your endpoint with middleware that checks for a payment header. If the header is not there, it responds with 402 Payment Required and a PAYMENT-REQUIRED header that encodes the price, the accepted token (USDC), chain (e.g. Polygon), and the facilitator's address. This is everything the client needs to form a valid payment. In terms of implementation, due to @x402/core containing a lot of what you need, this essentially boils down to having something like, e.g.
import { x402HTTPResourceServer } from '@x402';
const routes = {
'GET /api/data': {
accepts: [
{
scheme: 'exact',
network: POLYGON_NETWORK,
price: POLYGON_USDC,
payTo: PAYMENT_ADDRESS,
},
],
description: 'My protected endpoint',
mimeType: 'application/json',
}
}
The client receives that 402, reads the payment requirements, and prompts the user (or an agent, if we're talking about AI-to-API payments) to sign a transferWithAuthorization. The signed payload gets attached to the original request as a PAYMENT-SIGNATURE header, and the request is retried.
On mobile, a regular browser like Safari doesn't have access to window.ethereum, so the user can't sign anything. Then again, you do have actual mobile wallets. We can generate deep links to popular wallets like MetaMask, Rainbow, or Rabby. For example, the MetaMask link becomes https://metamask.app.link/dapp/x402.matija.eu/api/data. When the user taps it, MetaMask opens its built-in dapp browser and goes to that URL. The server responds with the same 402 page, but now window.ethereum exists because MetaMask injects it. From there, the user taps "Connect Wallet & Pay," which triggers the EIP-712 typed data signing and submits the payment.
The deep link doesn't pass any payment data to MetaMask - it just gets the user into a browser where window.ethereum is available. All the payment info (amount, asset, recipient, etc.) comes from the server-rendered HTML page itself. It's basically: "I can't talk to your wallet from Safari, so let me redirect you to a browser that can."
Now the facilitator comes into play. It intercepts the retried request, pulls the signature from the header, verifies it's valid and covers the required amount, and submits the actual on-chain transaction to Polygon. Once the transfer is settled, facilitator forwards the request to the origin server, which returns the resource together with settlement headers (transaction hash, block confirmation, etc).
From there on, it offers a hole host of options on how to exactly handle this payment, whether to treat it as a gateway to something other than the endpoint itself, e.g. a normal subscription page, or a donation page or what have you.
References