API and MCP
One route feeds everything: the box below, the MCP tool and the agent view all read GET /v1/check.
API
JSON over HTTPS, CORS open, no keys, no rate limit for now. Every route reads the public export in https://github.com/sarmadahsan/paidfor-data with a 10-minute cache, so a new export shows up without a redeploy.
| route | what it returns |
|---|---|
GET /v1/summary |
the headline numbers from summary.json |
GET /v1/sellers?min_calls=30&sort=rate |
seller rows; sort is rate, calls, volume or domain; category, zero_failures=1, limit filter |
GET /v1/seller/{domain} |
one seller: the row, its resources, its evidence and a verdict |
GET /v1/check?url={resource url or domain} |
the verdict for whatever you are about to pay |
GET /v1/demand?days=7 |
hosts ranked by /v1/check lookups over the last days UTC days (1 to 30), each with the index's paid calls and verdict; counted by host only, never the path, the IP or the caller |
GET /v1/usage |
call counts by route for this instance |
POST /v1/telemetry |
ingest for @paidfor/x402-safe-fetch outcome telemetry; exactly the documented payload, 204 on success, 400 on any other shape; counted as telemetry_rows on /v1/summary |
GET /v1/reported |
every host with an operator-reported series (mature or not), its reported fields flat and the measured failure_rate, wilson_low, wilson_high, calls and checks_7d next to it |
GET /v1/telemetry/{payer}/{nonce} |
one v2 row's verification: { status: verified | unverified | pending, reason?, block?, verified_at? } |
Every human page has a Markdown twin at the same path with .md appended (/index.md, /seller/api-xpay.com.md), and Accept: text/markdown on any page returns the twin. /llms.txt and /llms-full.txt are generated from the same JSON.
Verdict rule
One function, lib/verdict.ts: avoid when the 95% Wilson lower bound of the failure rate is above 10% and the seller has at least 30 paid calls; caution when the point estimate is above 2% or the sample is under 30 calls; ok otherwise. A low-sample seller (5 to 29 calls) is never avoid.
GET /v1/check response shape (the contract)
This one route feeds the check box on the home page, the MCP tool and the agent view's worked example. The shape below is frozen.
{
"input": string, // what you sent, trimmed
"host": string | null, // resolved host, lowercase, no scheme or path
"verdict": "avoid" | "caution" | "ok" | "unknown",
"verdict_text": string, // one sentence an agent can act on
"seller": SellerRow | null,
"resource": ResourceRow | null, // only when the input was a full URL we have 10+ paid calls to
"probe": { probes, modal_price, min_price, max_price, rate_402, // display strings, as shipped
modal_price_usdc, min_price_usdc, max_price_usdc, // six-decimal strings (added 2026-10-28)
rate_402_fraction } | null, // number 0..1 (added 2026-10-28)
"seller_url": string | null, // human page
"evidence_url": string | null, // raw evidence JSON
"methodology_url": string,
"generated_at": string // ISO 8601, from the export
}
SellerRow = { domain, tier, category, calls, failures, failure_rate, wilson_low, wilson_high,
by_type: { non_delivery, overcharge, duplicate, wrong_goods }, dollar_weighted_rate,
median_quote_usdc, last_seen, inflow_30d_usdc, bazaar_listing_count, low_sample,
verdict, url, evidence_url }
ResourceRow = { url, method, listed_price_usdc, modal_quote_usdc, max_quote_usdc, overcharge_ratio,
calls, failures, failure_rate, wilson_low, wilson_high, by_type }
The shape is additive only: fields may be added, never renamed or removed. seller.checks_7d (added 2026-10-28) is how many times this seller was looked up through /v1/check in the last 7 UTC days, this lookup included, or null when the counter is unavailable. GET /v1/seller/{domain} carries the same seller row plus resources, evidence, seller_reply: { received, status, url } | null (a seller's published response, see the seller page) and checks_7d. GET /v1/summary adds the counts checks_24h, checks_7d, hosts_checked_7d and pro_waitlist. A bare domain works too (?url=api-xpay.com). For unknown the verdict_text says exactly what we do not have: no paid calls to the host in the study with the probe count and modal unpaid quote when we have one, or "never seen".
Example
curl -s 'https://paidfor.dev/v1/check?url=api-xpay.com'
{
"input": "api-xpay.com",
"host": "api-xpay.com",
"verdict": "avoid",
"verdict_text": "Avoid: 55.1% of 512 paid calls to api-xpay.com failed the buyer; 282 settled on-chain and returned nothing usable within the seller's declared timeout. Evidence: https://raw.githubusercontent.com/sarmadahsan/paidfor-data/main/evidence/api-xpay.com.json",
"seller": {
"domain": "api-xpay.com",
"tier": "top",
"category": "other",
"calls": 512,
"failures": 282,
"failure_rate": 0.55078125,
"wilson_low": 0.5074765973770835,
"wilson_high": 0.5933295691392183,
"by_type": {
"non_delivery": 282,
"overcharge": 0,
"duplicate": 0,
"wrong_goods": 0
},
"dollar_weighted_rate": 0.55078125,
"median_quote_usdc": "0.005000",
"last_seen": "2026-10-29T06:29:43.444Z",
"inflow_30d_usdc": "8773.645000",
"bazaar_listing_count": 14,
"low_sample": false,
"verdict": "avoid",
"url": "https://paidfor.dev/seller/api-xpay.com",
"evidence_url": "https://raw.githubusercontent.com/sarmadahsan/paidfor-data/main/evidence/api-xpay.com.json",
"reported": null,
"verdict_basis": "measured",
"measured_verdict": "avoid"
},
"resource": null,
"probe": {
"probes": 136,
"modal_price": "$0.005",
"min_price": "$0.005",
"max_price": "$0.005",
"rate_402": "100.0%",
"modal_price_usdc": "0.005000",
"min_price_usdc": "0.005000",
"max_price_usdc": "0.005000",
"rate_402_fraction": 1
},
"seller_url": "https://paidfor.dev/seller/api-xpay.com",
"evidence_url": "https://raw.githubusercontent.com/sarmadahsan/paidfor-data/main/evidence/api-xpay.com.json",
"methodology_url": "https://paidfor.dev/methodology",
"generated_at": "2026-10-29T06:31:05.408Z",
"verdict_basis": "measured"
}
POST /v1/telemetry
The opt-in telemetry of @paidfor/x402-safe-fetch posts one object per paid call. Exactly these eight fields, nothing else; any extra or missing field is a 400. 204 on success. CORS open, no keys, 60 posts a minute per IP. Rows from the host selftest.paidfor.dev are stored but never counted.
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://paidfor.dev/v1/telemetry -H 'Content-Type: application/json' -d '{
"v": 1, "host": "api.example.com", "path_hash": "<sha256 hex of the path>", "outcome": "ok",
"amount_usdc": "0.010000", "latency_ms": 412, "validator_failures": [], "client": "x402-safe-fetch/0.1.0"
}'
outcome is one of ok, validator_failed, refused_verdict, refused_ratio, refused_max_per_call, no_acceptable_quote, paid_no_response, error. The count of accepted rows is telemetry_rows on GET /v1/summary.
Version 2 (telemetry: "verified" in the client). The same eight fields with the harness outcome names (ok, non_delivery, overcharge, wrong_goods, duplicate, plus the client-only ones), path_hash as lowercase sha256 hex, and on every paid row a proof object: network (eip155:8453), scheme (exact), asset (USDC on Base), pay_to, payer, nonce (32 bytes), tx_hash or null, settled_at, response_sha256 or null, declared_timeout_s, all lowercase. Refusals carry no proof and are counted by reason only. 202 with { status: "pending", verification_url } on a new row; 200 with status: "duplicate" when the same payer and nonce was already reported (nothing changes); 400 on anything else. Each row is checked on Base within 5 minutes and counts in the seller's reported series only when verified; see Two series. GET /v1/summary adds telemetry_verified_rows, telemetry_unverified_rows and telemetry_operators.
/v1/check (on seller) and /v1/seller/{domain} (top level and on seller) carry reported: { operators, calls, failures, rate, wilson_low, wilson_high, by_outcome, refusals, by_reason, mature, first_at, last_at } | null, and both responses carry top-level verdict_basis: measured, or measured+reported when the reported series is mature (30 counted calls from 3 payers) and the verdict is the worse of the two series.
GET /v1/changes
What changed, newest first, at most 500: ?since=<ISO time> and ?host=<domain> narrow it. No auth. Each event is { id, at, type, host, url, data } with type one of verdict_changed (from, to, rate, interval, calls), new_failures (delta by type), quote_changed (old, new, resource or null for the host median), pay_to_changed (old, new), seller_added, reached_min_calls, seller_reply_published, reported_series_matured, disagreement, audit_completed. The index is diffed hourly. RSS of the last 100: https://paidfor.dev/changes.xml; the human page is https://paidfor.dev/changes.
Webhooks
Alerts without an email address or a sign-in: a webhook is owned by the token you get when you create it.
curl -s -X POST https://paidfor.dev/v1/subscriptions -H 'content-type: application/json' -d '{"webhook_url":"https://example.com/paidfor-hook","hosts":["api.example.com"]}'
The 201 response carries id, manage_token and secret, once; store them. A test delivery is sent straight away (test_delivery says how it went). Then:
GET /v1/subscriptions/{id},PATCH({ hosts }replaces the list,{ add, remove }edit it,{ webhook_url }moves it) andDELETE, each with the headerX-Manage-Token: <manage_token>. Responses never include the URL.- Each hourly run with changes on your hosts sends one
POSTwith JSON{ v: 1, sent_at, events: [...] }(events as in/v1/changes) and the headerX-Paidfor-Signature: sha256=<hex HMAC-SHA256 of the raw body with secret>. 5-second timeout, two retries with backoff; after 20 consecutive failed deliveries the subscription is disabled until you PATCH awebhook_url. new_failuresandquote_changedare sent at most once per host per 24 hours; the other types always.- Up to 50 hosts; 5 are free, beyond that see pricing.
Watching by email works from any seller page ("Watch this seller") or the allowlist box on the home page: one field, a confirmation link, and every mail carries the links to manage or stop. The MCP server has no tool for subscriptions, since one would hold a token.
MCP server
Three read tools: check_x402_seller({ url }) returns the /v1/check result plus the one-sentence verdict; list_reliable_sellers({ category?, limit? }) returns zero-failure sellers with at least 30 paid calls; recent_changes({ host?, since? }) reads /v1/changes. Hosted Streamable HTTP endpoint: https://mcp.paidfor.dev/mcp (a plain GET on it returns the service status with the data generated_at it last saw). Stdio via npm: npx @paidfor/mcp. Same tools, same output, either way.
Install
Per-client setup (Claude Code, Claude Desktop, claude.ai, Cursor, VS Code, Windsurf, Codex CLI, ChatGPT) with one-click buttons and paste prompts: https://paidfor.dev/install. For agents, the same as Markdown: https://paidfor.dev/install.md.
Call it by hand
curl -s -X POST https://mcp.paidfor.dev/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_x402_seller","arguments":{"url":"keyring-agent.blockchhub.link"}}}'
The first call after a quiet period can take a few seconds while the host wakes the server; a cron sends a plain GET to https://mcp.paidfor.dev/mcp every 10 minutes to keep that rare.
Data
Everything comes from https://raw.githubusercontent.com/sarmadahsan/paidfor-data/main/: summary.json, sellers.json, resources.json, stress.json, probe.json, evidence/<domain>.json. Field documentation is in the data repo's README.
The index is free for everyone; teams and platforms: https://paidfor.dev/pricing.
Watch my allowlist
Get one mail when a seller your agent pays changes verdict, starts failing, moves its price or its pay-to address. Everything that changed.