# 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: 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`](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:31:05.408Z)

Human view: https://paidfor.dev/view?set=human&to=/api (sets a one-year cookie; or append ?view=human to any page)
