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 95100% 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.

tools/call · fetch_readable · no handle yetisError: true
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.
after the user pays · same tool, same argumentsauthorised
tools/call  fetch_readable
{ "url": "https://example.org/", "tollboothToken": "tb_s_…" }

→ 200  { "title": "Example Domain", "characters": 142, … }
   credits remaining: 249

This 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. 1 · Agent

    Calls a paid tool

    No handle yet. An ordinary tools/call.

  2. 2 · Tollbooth

    Returns a challenge

    isError: true, a checkout URL, and an opaque handle. Returned immediately — never blocks waiting for a human.

  3. 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. 4 · Agent

    Retries with the handle

    Same tool, same arguments, plus tollboothToken. Tollbooth polls settlement on that retry.

  5. 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 shapenretriedtoken exactdelivered
structuredContent + text18181818 (100%)
text only43414141 (95%)
URL-mode elicitation10000 (0%)
71/71

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.

0/71

Handle in user-visible text

The model never printed the handle for the user. Reassuring, not proof — a longer conversation may behave differently.

25

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.
The complete tool_result the model received.

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.

VariantIntentretried
v1control: URL and token, no instruction5/5
v2imperative: name the retry5/5
v3shipped · plus 8/8 on a confirmation run5/5
v4two-step framing4/5
v5maximally explicit4/5

Live settlement

pending — not yet measured

One 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.

1 · run the reference server
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
2 · make a tool paid
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))
);
3 · or point a client at the deployed one
{
  "mcpServers": {
    "tollbooth-research": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://tollbooth-server-production.up.railway.app/mcp"]
    }
  }
}
Nothing secret goes in this file. The deployed server holds the keys.

Full quickstart, with the three tools and the hardening notes: examples/research-tools →

Tollbooth

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.