Disclosure: I'm the maker of ScrapeMole, the API this post is about. All the numbers below come from our own test reports. There are no paying users yet.
AI agents are good at finding tools and bad at signing up for them. An agent can read an OpenAPI spec in milliseconds, but it can't open an account, confirm an email and paste an API key without a human. x402 skips that step: the server answers HTTP 402 Payment Required with a price, the client pays with a signed stablecoin authorization in a header, and gets the response.
ScrapeMole is a test of that idea: a few public web data endpoints (YouTube search, channel videos, transcripts, Google Trends, Amazon product pages) at $0.005 to $0.01 per call, paid in USDC on Base. This post covers how it works, how it runs on Cloudflare's free plan, and the problems we hit along the way.
The payment flow
Call any paid endpoint without paying:
curl -i "https://scrapemole-api-mainnet.scrapemole.workers.dev/v1/youtube/search?q=espresso"
HTTP/2 402
payment-required: eyJ4NDAyVmVyc2lvbiI6Mi... (base64 of the JSON below)
{
"x402Version": 2,
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"amount": "10000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0xCb535d13C7fC5Fe6C2B88da3f3601B840CCaf8Bc",
"maxTimeoutSeconds": 300
}]
}
(Trimmed: the real response also has a resource description and a Bazaar discovery extension with the input schema and an example output.)
amount is in USDC base units (6 decimals), so 10000 is $0.01. The client signs an EIP-3009 transferWithAuthorization for exactly that amount and retries with a PAYMENT-SIGNATURE header. With @x402/fetch it's a wrapper around fetch:
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY); // a wallet with a little USDC on Base
const pay = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(account) }],
});
const res = await pay('https://scrapemole-api-mainnet.scrapemole.workers.dev/v1/youtube/search?q=espresso&limit=10');
console.log(res.status, await res.json());
The buyer pays no gas: the facilitator submits the authorization on-chain.
"Failed calls are free" has to be enforced, not promised
This was the property we cared about most. A scraping API fails for reasons the caller can't control: the video has no captions, the channel doesn't exist, the source rate-limits you. Charging for those calls would make agents avoid us.
The x402 flow makes this simple to enforce. The server sends the payment to the facilitator (we use PayAI) for verification before doing any work, and requests settlement only after the handler has returned a 200. Every other status is a JSON error with "charged": false, and the authorization is never settled:
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_INPUT |
Missing or malformed parameter |
| 404 |
NOT_FOUND / NO_DATA
|
Target doesn't exist, or has nothing to return |
| 503 | BLOCKED |
The source rate-limited us even after retries |
| 502/504 | upstream error / timeout | Try again |
We test this end to end on Base Sepolia with throwaway wallets, comparing on-chain USDC balances before and after the run. The latest run passed 13/13 checks: four unpaid calls got correct 402s, four paid calls returned data, four deliberately failing paid calls (unknown video, unknown channel, missing query, impossible time range) returned errors, and the route we had disabled was confirmed to be absent. The buyer's balance went down by exactly $0.035, the sum of the four successful calls. The failing calls cost $0.
If you build something similar, put the balance check in your test. It's the only way to be sure that "not charged" really holds.
Running on the Cloudflare Workers free plan
The free plan gives you roughly 10 ms of CPU per request (network wait time doesn't count) and 100k requests a day. For endpoints that call a JSON API and reshape the result, like YouTube's internal API and Google Trends, 10 ms was enough. Three things needed work.
1. Egress regions get bot-checked differently
Datacenter IPs get bot-checked, and Cloudflare's are no exception. What surprised us was how much it depended on where the Worker ran. On our first live probe from Workers, YouTube transcripts succeeded 1 time in 5 and Google Trends 0 in 5.
What fixed it: when a run comes back BLOCKED, re-run it inside a Durable Object created with a locationHint, which places it, and its outbound requests, in another region.
for (const hint of ['weur', 'eeur', 'sam']) {
const stub = env.SCRAPER.get(env.SCRAPER.idFromName(`relay-${hint}`), { locationHint: hint });
const r = await (await stub.fetch('https://relay/run', {
method: 'POST', body: JSON.stringify({ id, input }),
})).json();
if (!isBlocked(r.items)) return r.items;
}
In our tests, Western Europe, Eastern Europe and South America passed where the other locations were blocked. With the retry, the next probes passed 10/10 for transcripts and 10/10 for Google Trends. YouTube search and channel listings never needed it (5/5 each). It uses no proxies, and since the retry only runs when the first try was blocked, it costs nothing in the normal case.
This is not a guarantee: the blocking is the source's decision and can change at any time. That's one more reason a blocked call must never be charged.
2. Some parsing simply doesn't fit in 10 ms
Parsing a full Amazon product page with cheerio went over the CPU limit. Rather than pay for a plan, that one endpoint runs on a Vercel function. The Worker links to it from its / and llms.txt, so agents still find it.
3. Don't share in-flight I/O across requests
The x402 server middleware syncs with the facilitator (GET /supported) when it initializes. If that happens during one request and a second request waits on the same promise, workerd refuses: a Worker can't await I/O that another request started. Our fix: build everything that doesn't need I/O (routes, discovery schemas) at startup, initialize inside the first paid request, and cache it only after it succeeds. If the facilitator is unreachable, the call returns a 503 (not charged) and the next request tries again.
Discovery: there's no app store
Agents only find you if you publish machine-readable metadata. We serve:
-
openapi.json, with anx-payment-infoblock (price, network, asset) on every paid operation -
/.well-known/x402, plus the Bazaar discovery extension inside every 402 (input schema and an example output) -
llms.txt, a plain-text guide for LLMs
One compatibility note: x402 v2 puts the payment challenge only in the PAYMENT-REQUIRED header, and some indexers built for v1 still read accepts from the body. We copy the decoded challenge into the body as well, and one indexer that had rejected us accepted the listing after that.
Several x402 directories (x402scan, x402dash, Agent402, 402 Index) accepted our listing with one API call each, with no account.
The same tools over MCP
There's also a remote MCP server at /mcp (Streamable HTTP, stateless) using the x402 MCP transport:
curl -s https://scrapemole-api-mainnet.scrapemole.workers.dev/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
tools/list is free. An unpaid tools/call returns isError: true with the same PaymentRequired object in structuredContent. The client retries with the signed payment in params._meta["x402/payment"], and the settlement receipt comes back in result._meta["x402/payment-response"].
Internally, each MCP tool just calls the matching HTTP route with a PAYMENT-SIGNATURE header, so there's one verify/settle code path for both interfaces. The MCP version of the testnet test passed 9/9: one paid search settled at exactly $0.01, and a paid call with invalid input returned INVALID_INPUT without settling. The server is in the official MCP Registry as dev.pages.scrapemole/scrapemole; the registry accepted proof of domain ownership through a file on our site, with no GitHub account needed.
What it costs to run
Nothing so far. Workers, Durable Objects, cron monitoring and the website are on Cloudflare's free tiers, and Amazon runs on a Vercel free function. PayAI's facilitator also has a free allowance for settlements, and the buyer pays no gas.
Open questions
- Will agent builders actually pay per call? It's live on Base mainnet, but no one has paid yet. Prepaid credits with an API key might still be easier for teams.
- How long will the regional retry work? I expect to keep measuring it with the cron canary that runs every 30 minutes.
- What should come next? If there's a source you'd want behind a 402, I'd like to hear it.
Docs, prices and live test numbers: scrapemole.pages.dev. The API's openapi.json and llms.txt are linked there.
Top comments (0)