71 blind trials · 2026-09-07
Agents come back after a payment challenge. We measured it.
In 71 blind trials, agents retried the tool after the user paid 95–100% of the time with structured and text challenges, and 0% with URL-mode elicitation. Tollbooth is a paywall layer for MCP servers built on that measurement.
- structured + text
- 18/18
- 100% retried
- text only
- 41/43
- 95% retried
- URL-mode elicitation
- 0/10
- 0% retried
Claude Code (headless), models sonnet and opus. The subject is blind: it sees a task, not an experiment.
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.
tools/call fetch_readable
{ "url": "https://example.org/", "tollboothToken": "tb_s_…" }
→ 200 { "title": "Example Domain", "characters": 142, … }
credits remaining: 249This is the shipped challenge copy, verbatim. In 71/71 scored trials the handle came back byte-identical whenever the model retried at all.
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 we measured
71 scored trials, sample sizes stated
2026-09-07. Claude Code (headless), models sonnet and opus. Each trial is a blind two-turn conversation: a task that needs the paid tool, then “I’ve paid, please continue.” Retry and token fidelity come from the server log, not from reading transcripts.
| Challenge shape | n | retried | token exact | delivered |
|---|---|---|---|---|
| structuredContent + text | 18 | 18 | 18 | 18 (100%) |
| text only | 43 | 41 | 41 | 41 (95%) |
| URL-mode elicitation | 10 | 0 | 0 | 0 (0%) |
Token exact equalled retried
Whenever a model retried, it reproduced the handle perfectly. Transcription is never the failure mode; the decision to retry is the only thing that varies.
Handle in user-visible text
The model never printed the handle for the user. Reassuring, not proof — a longer conversation may behave differently.
Trials discarded
A first sweep used an RFC 2606 reserved domain (example.com) as the checkout URL; models correctly refused. Re-run on a real checkout domain, the same copy scored 23/25. The domain is load-bearing.
Why elicitation scores zero — and why that is structural
MCP’s URL-mode elicitation names payment as a use case. In 10 trials it delivered nothing. The client recognises the -32042 error, runs its own consent flow, and hands the model only the text on the right: no URL, no handle, no message.
It is not a client gap. -32042 is a JSON-RPC error, so it terminates the call and there is nowhere for a handle to ride. Even rendered perfectly, the model is left with nothing to retry with. Tollbooth encodes that in the type system: a renderer that cannot carry the handle cannot be a challenge’s token bearer, and a test proves it.
“URL elicitation was canceled by the user. The tool "lookup_market_data" could not complete because it requires the user to open a URL.”
Challenge copy: five variants, 5 trials each
Underpowered. Five trials per variant cannot separate 5/5 from 4/5. The only robust conclusions are that both shipped carriers work and elicitation does not.
| Variant | Intent | retried |
|---|---|---|
| v1 | control: URL and token, no instruction | 5/5 |
| v2 | imperative: name the retry | 5/5 |
| v3 | shipped · plus 8/8 on a confirmation run | 5/5 |
| v4 | two-step framing | 4/5 |
| v5 | maximally explicit | 4/5 |
Live settlement
pending — not yet measuredOne real payment through the deployed server, timed from link creation to completed. These fields are filled from that measurement and from nothing else.
- Settlement chain
- pending
- Token
- pending
- Asked / received
- pending
- Creation → completed
- pending
tx: pending
For the person paying
The payer side is different
Most paid-MCP work assumes the agent holds a wallet. Tollbooth assumes a human does, and asks as little of them as a checkout link can.
No account. No signup. No KYC.
The person paying opens a checkout link and pays. Moove's read endpoint is public precisely because the payer has no account and no key.
basis: Moove documentation
Any token, any of 37 chains
Moove routes and swaps to the tool author's settlement token. The payer chooses what they hold.
basis: Moove's own count (37 chains)
The tool author receives the full amount
For a payment link, the payer's wallet is debited enough to deliver your amount in full. The 0.02% protocol fee is the payer's; same-chain, same-token is free.
basis: Moove fee schedule · measured confirmation pending
Settles straight to your wallet
Tollbooth never holds anyone's revenue. The payment API has no way to send money onward, so there is nothing to hold and nothing to trust us with.
basis: Design property
Payments run on Moove. A checkout URL identifies the tool author — the read is public by design — so it is fine to hand to the person paying and not fine to paste somewhere public.
Quickstart
A paid tool in under ten minutes
You need Node 20, a Moove account with a handle and a default wallet, and an API key. Unpaid calls never reach your handler.
git clone https://github.com/Jagadeeshftw/tollbooth cd tollbooth npm install npm run build export MOOVE_API_KEY=mk_live_... # Only if your key names a different host. Do not guess it. # export MOOVE_API_BASE_URL=https://api.moove.xyz node examples/research-tools/dist/stdio.js
server.paidTool(
'fetch_readable',
'Fetch a web page and return its readable text.',
{ sku: 'research', cost: 1 }, // an expensive tool can cost more
{ url: z.string() },
{ readOnlyHint: true },
async (args) => readable(String(args.url))
);{
"mcpServers": {
"tollbooth-research": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://tollbooth-server-production.up.railway.app/mcp"]
}
}
}Full quickstart, with the three tools and the hardening notes: examples/research-tools →
A paywall layer for MCP servers. An agent calls a paid tool, a human pays, the agent retries. Every number on this page is one we measured, at the precision we measured it.