paidfor.dev
HumanAgent

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

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.

One click in the confirmation mail and you are set. No password, no sign-in: every mail carries a link to manage or stop, and the address is kept encrypted and used only to send. Prefer a webhook? No email needed.