[switch to human view]
# paidfor.dev API and MCP server
Check any x402 seller before paying: GET https://paidfor.dev/v1/check?url=<resource url or domain> returns a verdict (avoid, caution, ok, unknown) and one sentence; the MCP tool check_x402_seller({ url }) wraps the same route.
## 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
```bash
curl -s 'https://paidfor.dev/v1/check?url=api-xpay.com'
```
```json
{
"input": "api-xpay.com",
"host": "api-xpay.com",
"verdict": "avoid",
"verdict_text": "Avoid: 54% of 500 paid calls to api-xpay.com failed the buyer; 270 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": 500,
"failures": 270,
"failure_rate": 0.54,
"wilson_low": 0.4961751549476092,
"wilson_high": 0.5832148978059177,
"by_type": {
"non_delivery": 270,
"overcharge": 0,
"duplicate": 0,
"wrong_goods": 0
},
"dollar_weighted_rate": 0.54,
"median_quote_usdc": "0.005000",
"last_seen": "2026-10-29T06:05:01.948Z",
"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:13:50.264Z",
"verdict_basis": "measured"
}
```
### `POST /v1/telemetry`
The opt-in telemetry of [`@paidfor/x402-safe-fetch`](https://www.npmjs.com/package/@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.
```bash
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](https://paidfor.dev/methodology#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](https://paidfor.dev/changes.xml); the human page is [https://paidfor.dev/changes](https://paidfor.dev/changes).
<a id="webhooks"></a>
### 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) and `DELETE`, each with the header `X-Manage-Token: <manage_token>`. Responses never include the URL.
- Each hourly run with changes on your hosts sends one `POST` with JSON `{ v: 1, sent_at, events: [...] }` (events as in `/v1/changes`) and the header `X-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 a `webhook_url`.
- `new_failures` and `quote_changed` are sent at most once per host per 24 hours; the other types always.
- Up to 50 hosts; 5 are free, beyond that see [pricing](https://paidfor.dev/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](https://paidfor.dev/install). For agents, the same as Markdown: `https://paidfor.dev/install.md`.
### Call it by hand
```bash
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](https://paidfor.dev/pricing).
## How to use this
- This page as JSON: GET https://paidfor.dev/v1/summary
- Check any URL: GET https://paidfor.dev/v1/check?url=api.example.com
- MCP tool: check_x402_seller({ url: "api.example.com" })
- MCP install, any client: https://paidfor.dev/install.md (stdio: npx @paidfor/mcp)
- MCP over Streamable HTTP: https://mcp.paidfor.dev/mcp
- There are no API keys and no rate limit for now.
Source: https://raw.githubusercontent.com/sarmadahsan/paidfor-data/main/summary.json (generated_at 2026-10-29T06:13:50.264Z)
Human view: https://paidfor.dev/view?set=human&to=/api (sets a one-year cookie; or append ?view=human to any page)