x402 brings back HTTP’s 402 Payment Required, a status code that sat unused for decades. The idea fits in three lines:
- A client calls an endpoint. The server answers
402with a price and where to pay. - The client pays, in USDC on Base, and retries with proof of payment in a header.
- The server checks the payment and serves the response.
No account, no API key, no card. That’s what makes it work for AI agents, which have a wallet and a task but can’t sign up for anything.
I built MonkePay to make this one line for an API owner. The protocol was the easy part. The work was everywhere around it.
The gate is easy; trust is not
Putting a price on a route is a small piece of middleware. The real question comes straight after: when your own backend is told “this request was paid for”, should it believe that?
In MonkePay the middleware runs on the developer’s server, not mine. It reports each payment to my backend so I can record it, take the fee and pay out the rest. My backend has to assume that report could be wrong, replayed or forged.
So each report has to pass three checks.
Is it from who it says? Every event is signed with HMAC over a timestamp, a nonce and the payload, and compared in constant time. A stolen event can’t be altered without breaking the signature.
Is it new? Each nonce is stored in Redis with set-if-absent. The second time a nonce arrives, it’s rejected. That blocks replays even when the signature is valid.
Did money actually move? This is the check that matters, and the first two don’t answer it. A valid, fresh, signed event proves who sent the claim, not that the claim is true. So the backend fetches the transaction from Base, decodes the USDC Transfer log, and confirms the amount matches what was claimed. The transaction hash is unique in the database, so the same payment can’t be counted twice.
Don’t wait for the chain, ask it
Right after a payment, the receipt may not be visible yet. The tempting fix is to wait a few seconds and look again. That bakes a guess about timing into a money path. On a slow block it fails; on a fast one it wastes time.
The backend asks for the receipt and, if it isn’t there, retries. A payment is recorded when the transfer exists, not when enough time has passed.
Moving money afterwards
Recording a payment is half of it. The platform’s 1% then has to be swept and the rest paid out, on a schedule, by workers that can crash halfway.
- One worker at a time. Sweeps and payouts take a distributed lock in Redis. The lock is released and renewed by small Lua scripts that check the lock’s token, so a slow worker can never release a lock that has already passed to another.
- Claim, then move. Each transaction is claimed before any money moves, and the movement carries an idempotency key. If a sweep fails, its claims are released and the next run picks them up. Running the job twice is safe.
- Gas first. The sweep tops up gas before it starts, and warns when the treasury runs low, instead of failing halfway through.
One bug taught me the most. An early payout drew on funds across networks, balances that should never have met. Balances are now strictly per network, and a monetary-safety regression suite guards that rule on every change.
A payment failure is not an API failure
If the agent paid and the API answered, the request succeeded, even if my backend was briefly down when the SDK tried to report it. Failing the request at that point would punish the one party that did everything right. Recording and callbacks are allowed to fail without failing the response.
What I’d tell anyone building on x402
- Treat payment proofs from your own SDK as untrusted input.
- Verify against the chain, and make the transaction hash unique.
- Never record or move money on a timer. Wait for a state.
- Make every money-moving job idempotent and locked, then run it twice in your tests.
- Keep balances per network from the first day.