Charlie KrugThe Build Log

← All posts

HTTP 402 waited 30 years for a job. Now I mock it for free.

Tollgate is a local mock server and CLI for the HTTP 402 payment handshake, so you can build the challenge-pay-retry loop without a wallet, a settlement rail, or a real dollar.

There is a status code in the HTTP spec that nobody has ever seen in the wild. 402 Payment Required has been sitting there since the early nineties, reserved, waiting for a payment system on the web that never quite arrived. Thirty years later a wave of "pay-per-call" API proposals (x402 and friends) is finally giving it a job: a server answers a request with 402 plus a machine-readable price, the client settles payment out of band, and retries with proof attached. It is a genuinely useful shape, especially for autonomous agents that need to buy an API call mid-task. The problem is testing against it.

Why a sandbox

If you want to build either side of this today, you are stuck. To exercise your error paths and retries you either stand up a real settlement rail (money, wallets, a whole backend) just to watch your code handle a 402, or you hand-roll a throwaway mock that slowly drifts away from what a real server actually sends. Neither is good. There is no ratified spec yet, just a handful of reference implementations describing roughly the same handshake, so "roughly right" mocks are exactly the trap.

Tollgate is a local, dependency-free mock that speaks the 402 handshake correctly and predictably. It is one Go binary: a mock origin server that challenges the routes you configure, and a CLI client that plays the other side, so you can develop and test the two halves independently without either one existing yet. The whole point is protocol fidelity. A sandbox that doesn't match how real challenges are issued teaches you the wrong lesson, which is worse than no sandbox at all.

The handshake, and the two rules that make it real

Point the client at a protected route and you can watch the whole exchange:

$ tollgate request --url http://localhost:8402/paid --verbose
GET /paid -> 402
descriptor: {Amount:100 Asset:USDC Recipient:0xsandbox Nonce:... ExpiresAt:...}
GET /paid -> 200
proof: {Nonce:... Scheme:fake Signature:...}

The first request comes back 402 with a payment descriptor: how much, in what asset, to whom, plus a nonce and an expiry. The client constructs a proof of payment referencing that nonce and retries, and this time it gets 200.

The interesting part is what the mock refuses to accept. Two rules do the heavy lifting. Nonces are one-time: a proof is good for exactly the challenge it answers, so if your client replays a header that worked a minute ago, the mock rejects it, same as a real settlement-backed server would to stop double-spends. And challenges have a hard expiry: present a stale proof and it is dead, no grace. Those two behaviors are precisely the ones a naive happy-path mock skips, and they are exactly where real integrations break. Testing against a mock that enforces them means you catch replayed headers and stale proofs on your laptop instead of in production against a rail that charges you to find out.

fake is honestly named

The proof scheme that ships first is called fake, and it is not a euphemism. It accepts unconditionally. Its only job is to exercise the shape of the protocol, the headers and the retry loop, not to simulate the cryptographic security of settlement. Calling it fake in the CLI keeps that honest: nobody wires it up thinking they have real payment evidence. There is also an hmac-sha256 scheme, a shared-secret signature that is one step closer to real proof, and the wire format is documented as a first-class artifact rather than something you reverse-engineer from the code, because the whole value here is making the protocol legible.

The nicest trick is that a scenario is a checked-in file, not a manual curl session. You write a JSON scenario describing an expected challenge/response sequence, run tollgate test scenario.json, and it stands up its own server, runs the sequence, and exits non-zero on any mismatch. That turns "does my 402 flow still work" into a CI assertion that lives next to your tests.

Try it

Grab a binary from the repo or go build it, run tollgate serve --path /paid --amount 100 --asset USDC, and in another shell curl -i that route to see a real 402 challenge come back. Then run the CLI client against it with --verbose and watch the challenge, the proof, and the retry scroll past. It is the entire handshake, no money, no wallet, no rail.

Tollgate is live. Free, in your browser, no signup.

This post is part of the build log: every app my automated factory ships gets written up here, honestly. Browse everything at apps.charliekrug.com. Comments are open below.

Comments

Loading comments…