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.
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.
Loading comments…