How challenge-and-retry works
The loop, why it never blocks, and what the handle is for.
The flow
How it works
Challenge, pay, retry
The loop rests on one assumption no protocol guarantees: that the agent comes back. That is the thing we measured.
- 1 · Agent
Calls a paid tool
No handle yet. An ordinary tools/call.
- 2 · Tollbooth
Returns a challenge
isError: true, a checkout URL, and an opaque handle. Returned immediately — never blocks waiting for a human.
- 3 · Human
Pays on moove.xyz
No account, no key. Any token, any chain Moove supports. Settles straight to the tool author's wallet.
- 4 · Agent
Retries with the handle
Same tool, same arguments, plus tollboothToken. Tollbooth polls settlement on that retry.
- 5 · Tool
Runs, one credit spent
The handle now owns a credit pack. Every later call spends from it without another challenge.
What a challenge is
An ordinary tool result with isError: true, carrying the checkout URL and the handle in two places at once: as text the model reads, and as structuredContent an agent can parse. The text is the shipped copy, verbatim:
PAYMENT_REQUIRED Payment required: 5.00 USDC for Research tools — 250 credits. NEXT STEP — show the user this link and ask them to pay: https://moove.xyz/@<handle>/pay/<link-id> AFTER the user says they have paid, retry: same tool, same arguments, plus tollboothToken="tb_s_…" Rules: - Copy tollboothToken exactly. It is opaque; any edit invalidates it. - Do NOT answer the user's question from your own knowledge instead. - Do NOT stop and summarise. The task is not finished until you retry. - This is not an error you should report and abandon.
Two things are deliberate. It is a result, not a JSON-RPC error, because an error terminates the call and leaves nowhere for the handle to ride — that is the structural reason URL-mode elicitation scores 0/10 (results). And it is restrained: the pushiest copy variants were the ones that drew refusals.
Why there is a handle
MCP offers no stable caller identity. Sessions were removed from the protocol in revision 2026-07-28; Mcp-Session-Id does not survive a reconnect in 2025-11-25; stdio has no header layer at all. So Tollbooth mints a handle — tb_s_…, 16 bytes of CSPRNG output — and the agent carries it back as a tool argument. The specification itself now prescribes exactly this for cross-call state.design property
In every scored trial where a model retried, it reproduced the handle byte for byte (71/71).measuredTranscription is not the failure mode; the decision to retry is the only thing that varies. The handle’s lifetime and what binds it are on the security page.
Why it never blocks
The MCP TypeScript SDK’s default request timeout is 60 seconds. resetTimeoutOnProgress exists but is a client-sideoption defaulting to false, so a server cannot keep a call alive by sending progress. A human opening a browser, connecting a wallet and confirming a transfer is a 30-second-to-several-minute operation with an unbounded tail. Tollbooth therefore returns the challenge immediately and lets the agent’s own retry drive the rest.
How settlement is noticed
- On the retry.When a handle with no credit is presented, Tollbooth looks for that handle’s pending charge and asks the provider whether it settled. This is the natural trigger and settles almost everything.
- Rarely, in the background. A reconciler sweeps pending charges on a schedule and abandons expired ones. See rate limits and polling.
- Exactly once. Both paths can observe the same settled payment;
claimSettlementlets exactly one of them grant. See stores.