x402 Doctor
[x402 payable] [Listed on x402-list] · Live status · [x402 Doctor MCP connector on Glama]
Diagnoses why an x402-payable endpoint's payment flow is broken, without needing a funded wallet.
Paste a URL (web app) or run x402-doctor <url> (CLI, CI) and get back exactly which check failed, why, and how
to fix it. Every check traces back to a real bug hit while shipping
Ichimoku Signal and presign-guard.
[x402 Doctor in 70 seconds]
▶ Watch the 70-second explainer (with voice and captions):
diagnosing a broken endpoint, the pre-payment check for agents, and the daily Trust Index.
[x402 Doctor: the paid fix]
▶ New: the paid fix in 70 seconds: for $0.05 the Doctor hands you
the medicine, the exact code that fixes each problem for your stack, in the browser or as JSON for agents.
What it checks
Statuses: pass, warn (works, but something is off), fail (payments fail or it is not valid x402), info.
Web app
Reports are shareable: https://<host>/?url=<endpoint>&method=GET runs the diagnosis on load (&method=POST&body=<json> for a POST with an example body).
API: POST /api/diagnose with { "url": "...", "method": "GET" | "POST", "body": { ... } } (method and body optional)
returns { url, method, overall, checks[], challenge, share_url }. body is an example request body (JSON, up to
16 KB) for a POST that checks its input before the 402; it implies POST, and the report says which body reached the 402
(body, and body_source when it came from the seller's /openapi.json). share_url is the page link above, to paste into an issue or a chat. Rate-limited to 10 diagnoses per minute per IP.
Paid API for agents (x402)
GET /api/v1/diagnose?url=<endpoint>&method=GET|POST returns the same report, $0.01 USDC per call on Base or
Solana via x402, with no rate limit. It is meant for agents and CI pipelines; the web page stays free.
- Unpaid requests get the x402 challenge (
PAYMENT-REQUIRED header, mirrored in the body) with a Bazaar input and
output schema.
body=<URL-encoded JSON> (optional, also on preflight, deep and fix, and as body in the MCP tools): an example
request body for a POST route that checks its input before the 402. It implies POST; with method=GET, or when it
is not JSON, the answer is a 400 before payment. The batch takes no body.
- Invalid input (bad URL, non-http(s), unknown method) gets a 400 before payment. The bare route without
url
answers 402, so indexers such as x402scan can register it. Paying without a url returns a 400.
- The x402 middleware settles only after a 2xx response, so a diagnosis that errors out is never charged.
- Discovery:
/openapi.json (with x-payment-info) and /.well-known/x402.
- Browsers opening the route get a wallet paywall (Base first: MetaMask, Coinbase Wallet) instead of the bare 402.
- Bazaar: a facilitator lists a route after the first payment it settles for it. With the CDP keys set, one paid
call (e.g. from the browser paywall) puts the route in the CDP Bazaar that agents search.
Pre-payment check for buyers: GET /api/v1/preflight
GET /api/v1/preflight?url=<endpoint>&max_usd=0.05&network=<caip2>&method=GET|POST, $0.001 USDC per call:
call it before your agent pays an x402 endpoint it has not used before. It never pays the endpoint. Add
client=gateway when your agent pays with Circle's Gateway client from a Gateway balance: Circle Gateway-only
endpoints (gateway_only, otherwise a caution) are then a go, and a Gateway option is recommended first.
BNB Chain (eip155:56) has no Circle USDC. Sellers price in 18-decimal dollar tokens: Binance-Peg USDC, USDT,
USD1 and United Stables (U), so $0.01 is "10000000000000000". Doctor prices those options like USDC. USDC and USDT
there have no EIP-3009, so an option without assetTransferMethod: "permit2" fails; a value standard x402 clients
don't know is a warning, because @x402/evm then signs EIP-3009 instead. "permit2-exact" and "permit2-upto" are the
methods of Binance's B402 facilitator, so only its own clients pay those options.
Bait check (TypeSafe's Jev, when TYPESAFE_API_KEY is set): for an endpoint that could be paid, Jev judges
what the protocol checks can't. Does it claim to be, or to be official from or endorsed by, a brand that doesn't own
its domain (brand_impersonation; reselling or proxying a brand's product under the seller's own name, such as an
LLM gateway naming the models it serves, is not), lure with free tokens, airdrops or guaranteed returns (lure_description), or
promise something its declared output doesn't deliver (output_mismatch)? A clear yes is a caution, an
in-between answer info; it never makes a verdict no_go and never turns one into go. The probabilities are
in signals.jev_lure. On 300 real CDP Bazaar listings it raised no caution for brand or lure wording. When a bait
question changes (BAIT_VERSION in lib/jev-lure.js), the Trust Index's next run asks again, first, every service
the old wording flagged or was unsure about; clear answers carry over.
Namesakes (same key, from the daily Trust Index scan, lib/namesakes.js): the x402 Bazaar indexes each
listing's service name, so an agent that searches for a known service finds every listing with that name. When
another seller, with its own payout address and fewer payers, lists under the brand name of an established service,
the preflight adds namesake (info: the name, the original's site and both payer counts). Sites that share a payout
address, a distinctive host-name word or three or more names count as one owner, and TypeSafe's Jev must be sure
(0.85) that the name is a brand rather than a description such as "web search". On 23 Oct 2026, 83 names were listed
by more than one owner and one was a brand with a better-known original. It never changes the verdict: a namesake
can be a fair reseller or an unrelated service.
Price check (same key): is the price far above what comparable services ask? Doctor takes up to 24 paid APIs
of other sellers from the x402 Trust Index whose descriptions share rare words with this one (at most two per
seller), and Jev judges per candidate whether a buyer would be served just as well by it. With at least 5 such
services, their median price is the comparison: 5x or more (and at least $0.05 more) is info, 20x or more (and
at least $0.25 more) a caution, both price_high and naming up to three cheaper alternatives. The comparison is
in signals.price_check (comparable_median_usd, comparables, ratio, cheaper[]). It never makes a verdict
no_go: the price is the seller's choice, the agent decides whether it is worth it.
Not being listed in the CDP Bazaar is reported as info only. Results are cached for 10 minutes per URL, budget
and network (cached: true), so checking before every payment stays fast.
What "go" covered: checked and not_checked. "go" means nothing wrong was found in what was checked, never
that the seller will deliver: a preflight reads the 402 and the seller's record, it doesn't pay or send your real
request. So every answer (lib/preflight-coverage.js) lists checked ([{ check, what }]: the payment terms,
sanctions, payout address, budget, advertised price, bait, price comparison, track record, on-chain payments,
purchases, agent reports, HTTPS) and not_checked ([{ check, because, why, send? }]), where because is
unavailable (a check that applies couldn't run: the sanctions list, the bait or price check), no_data (it ran,
nothing on record yet: no track record, no purchases, no on-chain count), not_sent (send names the parameter:
max_usd, or body for an endpoint that checks its input first) or out_of_scope. Two items are always out of
scope: whether the seller answers your own request (the delivery check buys it once, GET /api/v1/delivery-check)
and what your agent signs to pay (presign-guard's /v1/check, with the 402 as x402). checks_complete is
false when anything was unavailable. The deep preflight carries the same fields; batch rows carry
checks_complete. The same fields as presign-guard's answers.
Several endpoints at once: GET /api/v1/preflight/batch
GET /api/v1/preflight/batch?url=<a>&url=<b>&max_usd=0.05, $0.005 USDC per call for up to 10 endpoints (one
url parameter each; duplicates count once). For marketplaces, directories and agents that weigh several services.
Each endpoint gets the same verdict as a single preflight; one that errors or does not answer within 20 seconds is
unknown and does not hold up the rest.
Before an expensive call: GET /api/v1/preflight/deep
GET /api/v1/preflight/deep?url=<endpoint>&max_usd=0.5, $0.01 USDC per call: everything the preflight answers,
plus
diagnosis: the full diagnosis (overall, every check, and the failing or warning ones as problems);
history: the daily scans of the last 30 days, one { date, status } per day;
seller: the seller's other scanned endpoints on the same origin (how many, payable today, average payable ratio,
how many were unreliable). A seller whose endpoints were payable on less than half of the scans turns go into
caution (unreliable_seller);
domain: the domain-move checks (well-known, bazaar-listing), e.g. a Bazaar listing only under an old domain.
Paid and delivered? GET /api/v1/delivery-check
GET /api/v1/delivery-check?url=<endpoint>&method=GET|POST, $0.25 USDC per check: x402 Doctor buys the endpoint
once, with the example input its own listing declares, and publishes what came back. For sellers who want proof that
their endpoint delivers, and for buyers before a large order.
- Checked before you pay: the endpoint must be https, not a fizzl.eu route, get a
go from the pre-payment check,
be payable in USDC on Base and cost at most $0.10. Anything else answers 400 (or 503 when the day's 20 checks are
taken), and nothing is charged. A request with a payment always waits for the whole check before anything
settles. A probe without a payment wants the 402: it gets one unless the request itself is malformed (not one
https URL, a fizzl.eu route, a bad method), also for an endpoint that can't be bought now, since uptime monitors
fill url with a placeholder such as https://example.com, and within 5 seconds (the CDP Bazaar's validator and
health probe give up after 10). To get the answer before paying, send X-Delivery-Precheck: 1; the homepage does.
- The answer comes at once, the purchase shortly after: the paid call answers with a job
id and a status_url.
delivery-check.yml takes the queue (GET /api/v1/delivery-check/queue,
free) and buys with the dedicated spot-buy wallet (SPOT_BUY_KEY; no wallet key on the server). Only a job whose
payment settled is released to the queue. The server starts the workflow as soon as a payment settles
(DELIVERY_DISPATCH_TOKEN: a fine-grained GitHub token for this repository with only Actions read and write;
lib/delivery-dispatch.js); its schedule, every 10 minutes, is the fallback, since GitHub runs scheduled
workflows late or not at all.
- The same steps as the spot-buy (
lib/spot-buy.js): a fresh preflight with the OFAC payout screen right before
paying, the seller's declared example input, the answer checked against the declared output schema and judged by TypeSafe's Jev (is it what was
paid for, does it carry instructions for the agent: lib/jev-answer.js), and the charge measured on chain.
- Public result:
GET /api/v1/delivery-check/<id> (free) shows queued, then done with the result
(delivered, off_schema, empty, charged_not_delivered, payment_rejected, error_not_charged, or the step
that stopped it before paying) and the transaction. Every result is on the delivery-data branch.
- Caching: for a delivered GET answer,
cache says whether a shared cache (a CDN) may store the paid answer
and serve it to callers who did not pay (shared_cache: yes, no or unspecified, with the Cache-Control
it sent). The x402 payment travels in its own header, not in Authorization, so only Cache-Control: private or
no-store keeps it out (Five Attacks on x402, attack III).
Jobs are kept in the credits Redis (CREDITS_REDIS_URL); without it the route answers 503 before payment.
Prepaid credits: GET /api/v1/credits/1000
Agents that check often can prepay: one x402 payment of $0.80 for 1000 credits or $7.00 for 10,000 (20% and 30% off), valid for a year. A credit is $0.001: a preflight costs 1, a batch 5, a deep preflight or a diagnosis 10, the fix 50. The pack answers with a credit_key; send it as the x-credit-key header with any paid call instead of paying. GET /api/v1/credits (free) shows the prices and, with the header, your balance. A call that fails gives its credits back; with no key or too few credits the normal 402 applies. Needs CREDITS_REDIS_URL (a persistent Redis such as Upstash; the presign-guard instance is fine, the keys are apart); without it, no packs are sold.
The fix, as code: GET /api/v1/fix
GET /api/v1/fix?url=<endpoint>&method=GET|POST&stack=<optional>, $0.05 USDC per call: diagnoses the endpoint,
then returns, per failed or warning check, the concrete change that fixes it, as code for your stack and filled in
with your own values (payTo, amount, network, route).
The stack is read from the response headers (x-powered-by, server, Vercel/Render/Cloudflare headers) and the
challenge: express, next, hono, node (an @x402 SDK behind another framework), python or generic (raw
HTTP, for any language). Pass stack= when the guess is wrong. Node snippets use the @x402 v2 route config, which is
the same for @x402/express, /next and /hono; lines that only apply to Express say so.
Recipes (lib/recipes.js, one test each in test/recipes.test.js): no 402 at all (200 before the paywall, auth or
validation first, 404/405, unreachable), v1 or invalid challenge, wrong header name, no body mirror, no accepts,
scheme, legacy network names, invalid payTo (spots quotes, spaces, the wrong chain), USDC asset, decimal amounts,
Solana fee payer, EIP-712 domain, http resource URL behind a proxy, resource URL and metadata, Bazaar declaration and
example, Solana payout token account, Phantom on PayAI, testnet paywall on mainnet, and openapi.json. Anything
without a recipe is returned under unfixed with the diagnosis hint.
Learning from outcomes: POST /api/v1/outcome
After a paid preflight, a buyer can report (free) what happened when it then paid the endpoint: paid_ok, paid_failed (402 again) or paid_error. x402-safe-fetch does it with shareOutcomes: true.
- A report counts only with a real preflight that Doctor signed for exactly that query, once per preflight (
receipt.request_id), so a false report costs a paid preflight.
- Doctor counts reports per endpoint over 30 days. When most reported payments failed, from at least three different paying wallets, later preflights add
payments_fail_after_preflight (caution); signals.agent_outcomes shows the counts.
- Reports land in the usage log (endpoint, outcome, status, verdict, payer from the receipt); nothing else about the buyer.
Refused, and why. An agent that checked an endpoint and then did not pay it can say why: "outcome": "refused" with a reason (a short code: the preflight or presign-guard reason that stopped it, such as payto_changed, price_changed or payee_mismatch) and a source (doctor, presign-guard, policy or other), with the same signed preflight. Any verdict counts here, no_go too. Refusals never count as failed payments. They are the seller's signal back: the free web check shows them (buyer-refusals, info), and GET /api/v1/refusals?url=… gives the counts per reason for the last 30 days, never who refused.
Refusal extension (draft). A proposal for an open version of the same signal, straight from buyer to seller: x402-foundation/x402#4087. Doctor implements both sides (lib/refusal.js). Its own 402s carry extensions.refusal.info.url = https://x402-doctor.fizzl.eu/.well-known/x402-refusal, which takes a report (POST, answered 204) and shows the counts per resource and reason, and per day, resource and reason (GET). When a preflight's verdict is not go and the seller names a refusal url on its own site, the answer has refusal: { url, send_if, signing_message, report }: a ready report with the reason code (price_above_limit, payto_changed, trust_low, terms_mismatch, …) and the sha256 of the refused accepts[] entry. Doctor never sends it; a buyer that doesn't pay can post it, fire-and-forget. A screening_hit must be signed (EIP-191 over signing_message): unsigned, anyone could flag a seller with made-up sanctions hits, so the endpoint refuses it. "Couldn't check" is not "checked and failed": a check that could not complete (a lookup that didn't answer, a list that wasn't loaded) is check_unavailable, with detail naming the check (payto_binding, screening or trust), so a lookup outage never reads as an untrusted payTo or a sanctions hit. by is optional and best left out, so buyers aren't grouped by the client or checker they use. A buyer's limits are the owner's private policy (0.4-draft): detail never carries limits, budgets or other amounts, the policy reasons (price_above_limit, policy_blocked, trust_low) leave it out unless the owner allows it, and then price_above_limit's detail says which limit was hit (per_call, daily, total), never its value. The endpoint refuses a report that breaks this, and Doctor's ready reports carry no detail for these reasons. A signature shows which wallet sent a report, not that the owner agreed to disclose anything.
Signed verdicts
Every paid answer (diagnose, preflight, batch, deep and fix; HTTP and MCP) carries a receipt signed by Doctor, so an agent can later prove which verdict it got for which endpoint, for example why it did or didn't pay:
- Signed: the whole answer without
receipt.signature, as canonical JSON (profile js-json-stringify-sorted-utf16-ascii-v1: keys sorted by UTF-16 code units, compact, every code unit from U+007F up as lowercase \uXXXX, numbers as JavaScript's JSON.stringify writes them — 1.0 → 1, 0.000001 → 0.000001 — then UTF-8 bytes; Python's json.dumps matches only for ASCII keys and integers, so use examples/canonical.py, which also has verify_receipt(answer)), with EIP-191 personal_sign. Flipping the verdict or moving it to another request breaks it.
payment: the payment that bought this answer, from your x402 payment payload. On Base the payer and the EIP-3009 nonce, so anyone can find the settlement on-chain as the USDC contract's AuthorizationUsed(payer, nonce) event; on Solana the payer and a SHA-256 of the signed transaction you sent.
input_sha256: SHA-256 of the canonical JSON of {"route", "input"}, where input is your query parameters as strings (HTTP) or the tool arguments (MCP, route mcp <tool>).
- Signer:
/.well-known/x402-doctor-signer.json, with retired signers listed so old receipts keep verifying.
- Free check:
POST /api/v1/verify with {"response": …, "route": …, "input": …} returns valid, signer, known_signer and input_matches. Over MCP: the free tool verify_receipt with the same arguments (route like mcp x402_preflight, input the tool arguments).
Key rotation without client updates: the payout wallet (0x6B0F4651eD42893ab58139938175E4a69f175F25) authorises each signing key with a personal_sign over fizzl receipt signer / service: x402-doctor / signer: <address> / valid_from: <YYYY-MM-DD>. Set it as RECEIPT_SIGNER_CERT (YYYY-MM-DD:0x<signature>, produced by /sign-receipt-key in the wallet's browser). It is checked at startup and carried in every receipt as receipt.cert, so a client that pins only the payout wallet verifies a new key offline.
Same format as presign-guard, so one verifier works for both. The key comes from RECEIPT_SIGNER_SECRET (holds no funds); without it answers are unsigned.
x402 Trust Index
Once a day, trust-scan.yml runs the pre-payment check against every resource in
the CDP Bazaar, the MPP directory, the GoPlausible facilitator's discovery list (x402 on Algorand, about 2,500 endpoints the Bazaar doesn't list) Circle's Agent Marketplace catalog (about 2,800 endpoints, many paid through Circle Gateway) and the XRPL AI Hub (the merchants paid through t54's XRPL facilitator; it has no listing API, so its merchant pages are read, about 190 pages a day) and nohumans.directory's catalogue (CC BY 4.0, mirroring it is a supported use; about 9,600 active listings, some 1,550 of them in no other registry on 25 Oct 2026) and PayAPI Market's catalogue (its open /agent/list; about 940 listings and 1,000 routes, some 300 of them in no other registry on 25 Oct 2026) (read-only: it stops at the 402 challenge, at most two requests per host at a time, one
/openapi.json per origin, a User-Agent that links to /trust). The results roll into a 30-day history per resource,
one letter per day (g go, c caution, n no-go, x unreachable, - not scanned), published as
index/ + summary.json on the trust-data branch. The index is written in parts of at most 24 MB
(index/meta.json, then index/<nn>.ndjson with one [key, resource] per line; lib/index-files.js), so no file
nears GitHub's 100 MB limit as the index grows; index.json and index.ndjson, the whole index as one file, are
published as well while they stay under 90 MB. The scan runs in parallel jobs, each with whole sellers (a plan job
reads the catalogs, four scan jobs, a publish job merges them): a share that fails leaves its endpoints not checked
that day, never marked down. A seller with more than 5,000 endpoints gets a share of them checked each day, so that
each one has its turn within a week (the rest are - that day and keep their last verdict; one seller listed 62,924
on 27 Oct 2026). A seller whose endpoints answer with a server error in one scan while at least 50 of them were payable
at their last check has an outage, not 50 broken endpoints: those are - that day (code host_outage, listed in
summary.json as host_outage) and keep their last verdict, once; if they still fail at their next check they are
marked down. On 29 Oct and 1 Nov 2026 one seller's outage turned 7,000 to 9,000 endpoints no-go for a day.
The server doesn't load that index (42 MB, ~88,000 resources on 25 Oct 2026): after the scan, scripts/trust-views.js
(lib/trust-views.js) writes it as small precomputed files on the same branch, from the same code (views/meta.json
with the summary, views/atlas.json, views/templates.json, views/sellers.json, the price check's word index under
views/similar/, one views/networks/<key>.json per network (one over 16 MB as a head and parts, listed in
views/meta.json network_parts), and the resources in 256 buckets by key under keys/).
lib/trust-reader.js reads only what a request needs (one ~130 kB bucket per lookup, at most 32 kept) with the same
answers as the full index, and falls back to the full index while the views aren't published. Measured on the 20 Oct
index: 116 MB peak instead of 285 MB, 37 MB of heap instead of ~100 MB.
After each scan, hf-sync.yml publishes it as an open dataset on Hugging Face,
x402Fizzl/x402-trust-index (CC BY 4.0): data/endpoints.jsonl (every endpoint in the latest scan with its
verdict, reasons, price, networks, category and history), data/daily.csv (go / caution / no_go per day),
data/spot_buys.jsonl (every purchase of the weekly spot-buy, from the spot-data branch), data/onchain_payments.csv
(signed USDC payments on Base per payout address and day, from the payments-data branch) and the dataset card
(scripts/hf-export.js). It also keeps the dataset in the Hugging Face collection "x402 agent payments" when the token
may write collections. It needs the repository secret HF_TOKEN (a Hugging Face write token); the variable
HF_DATASET overrides the dataset name.
hf-bench.yml publishes the
x402 Agent Safety Bench the same way
(scripts/hf-bench.js): a dataset x402-agent-safety-bench with every run and its transcript (from x402-examples'
bench-data branch), the published tables as leaderboard.csv, and a static leaderboard Space of the same name. It
runs when the script changes, every Monday and by hand; the variable HF_BENCH_REPO overrides the name.
hf-mcp-space.yml publishes hf-mcp/ as the Space
x402Fizzl/x402-doctor: a Gradio app that is also an MCP server
(https://x402fizzl-x402-doctor.hf.space/gradio_api/mcp/, or added under Hugging Face's MCP settings), with four
free tools over the public API: check_x402_endpoint (/api/diagnose), trust_record (/api/trust),
add_to_trust_index (POST /api/trust/submit) and x402_market_health (/api/trust/summary). It never pays or holds a key; the variable HF_MCP_SPACE overrides the name.
Hugging Face hosts Gradio Spaces only for PRO accounts: without PRO the workflow warns and publishes nothing, and the
Space waits in hf-mcp/. Doctor's own MCP server is https://x402-doctor.fizzl.eu/mcp.
- The pre-payment check adds
signals.track_record and a caution (unreliable_history) when a seller was
payable on fewer than half of at least 3 scanned days.
- It also compares who gets paid and the price with the daily scans (
lib/payee-history.js): a caution
(payto_changed) when the payout address on a network is not the one the last scan saw, or changed in a scan of the
last 7 days (payto_changed_alone when that daily change was the only one of its site, the domain with all
its subdomains, while at least 3 other endpoints of the site kept their address: most changes come per seller, as
when a seller rotates its wallet), and price_changed when the price is higher than in the last scan (info when lower) or changed this
week. The index keeps a short fingerprint of each payout address, not the address.
- Fizzl Buy buy-check.
buy-check.yml, started by hand only, buys through
Fizzl Buy (POST https://buy.fizzl.eu/v1/buy, $0.02) the way an agent would (scripts/buy-check.js): 1 to 20
everyday needs (prices, weather, search, news), each with whether a seller delivered, which one, what it was paid,
TypeSafe Jev's "what was paid for" and the time. It pays with the spot-buy wallet (SPOT_BUY_KEY); a dry run (the
default) only asks the price. These are our own purchases: the public Fizzl Buy counts in GET /api/stats leave
them out, by the x402-doctor-buy-check user agent and by the wallet, like our other workflows and test bots.
- Spot-buy sample: does "go" mean paid and delivered? The scan only reads the 402 challenge. Once a week
spot-buy.yml buys a small random sample of "go" endpoints (lib/spot-buy.js):
x402 exact in USDC on Base only, one endpoint per host, never a fizzl.eu or test host, the seller's own declared
example as the request (when it reaches the paywall), a fresh preflight with the OFAC payout screen right before
paying, and hard caps (default 30 endpoints, $0.05 per call, $2 per run; at most 50, $0.10 and $5). Whether money
left the wallet is measured on chain (its USDC balance before and after), and the seller's PAYMENT-RESPONSE is
checked against the chain. Each result is delivered, off_schema (an answer that doesn't match the declared
output schema), empty, charged_not_delivered, payment_rejected, error_not_charged or payment_failed;
the run is appended to the spot-data branch (latest.json, results/<date>.json, a second run that day as results/<date>-2.json). It pays only with the secret
SPOT_BUY_KEY, a dedicated wallet holding a few USDC; without it every run is a dry run that pays nothing.
The latest run is on /trust ("Paid and delivered?") and at GET /api/trust/spot (summary and every purchase, no
wallet details); GET /api/trust?url=… adds spot_buy when that endpoint was bought in it (lib/spot-results.js).
The paid pre-payment check adds the same as signals.spot_buy, and a caution (charged_without_answer) when the
endpoint took the payment for the example request its own listing declares and answered with an error or not within
90 seconds. Each purchase also records its answer time (answer_s, from the first request to the paid answer): the
summary counts answers slower than 30 seconds (slow_answers, and delivered_within_30s_pct, what an agent that
waits 30 seconds gets), and a slow one adds slow_after_payment (info) to the pre-payment check.
When the answer check ran (TypeSafe Jev, with Claude when unsure), each purchase carries answer_check
(delivered: yes, no or unsure; injection), and for the seller's own example request the pre-payment check adds
answer_not_what_was_paid_for or answer_carried_instructions (both info: one purchase, never the verdict).
- Delivery evidence from others who buy endpoints with real USDC: nohumans.directory (its
catalogue is CC BY 4.0) and PayAPI Market, which pays one route of each listing once. The
scan's plan job writes the endpoints they bought and got an answer from to
delivery-nohumans.json and
delivery-payapi.json on trust-data (by key: the day of that purchase, the listing's status, a link to the listing;
about 3,650 and 970 on 25 Oct 2026, 600 of PayAPI's not in nohumans', against the 26 a week the spot-buy can
afford). The paid pre-payment check adds signals.delivered_elsewhere (the latest purchase, every source's in
sources) and an info line (delivered_elsewhere) for such an endpoint, GET /api/trust?url=… adds
delivered_elsewhere, and the web check and /trust show it (lib/delivery-elsewhere.js). It is information next
to the read-only check, never the verdict.
A third source, OpenClawProductions' delivery grades, is read straight from their feed once OPENCLAW_GRADES_URL is
set ({ "grades": [{ "url", "date", "delivered": true|false, "reason"?, "report"? }] }, the latest per endpoint).
It also has failed purchases (paid, and no usable answer): those add an info line failed_elsewhere with the
reason.
The same wallet makes one other kind of payment, started by hand: index-payment.yml
pays our own fizzl.eu routes (scripts/index-payment.js), because the CDP Bazaar lists a route only after a payment
for it settles and drops it after 30 days without one. stale (the default) pays each of our 16 routes whose last
settled payment in the Bazaar is 21 days old or more (read from its public search), each at most its own price
(INDEX_PAYMENT.routes); url pays one route once. The money goes to our own payout wallets. It publishes nothing,
so the sample stays one of other sellers. Each run is also a self-audit of those routes: the same payment header is
sent once more and must get a 402 ("Five Attacks on x402", attack II; a route that serves it again fails the run),
and the settlement transaction's ERC-8021 suffix shows whether our Base Builder Code was written into it. Other
sellers' endpoints are never replayed.
- Sanctions screen. Every payout address (x402
payTo, MPP recipient) is matched against the OFAC SDN list's
digital currency addresses, from presign-guard's free open copy of the
US Treasury list (lib/sanctions.js, refreshed every 6 hours). A listed address makes the pre-payment check no_go
(payto_sanctioned, with the SDN entry), in the paid check and in the daily scan; signals.sanctions says what was
screened and against which publication. summary.json counts the scanned resources that pay out to a listed address
(payto_sanctioned). Without the list nothing is judged (signals.sanctions.unavailable).
- Burn addresses. A payout address nobody holds a key for, mostly a placeholder left in the config, makes that
option unpayable: the zero address (USDC refuses the transfer),
0x000000000000000000000000000000000000dEaD, and on
Solana the System Program or the incinerator. The pre-payment check says payto_burn: no_go when no other option
is payable, caution otherwise (a client that signs the first option it can would pay the broken one).
summary.json counts them (payto_burn).
One exception: an option that pays a token other than a dollar token to 0x…dEaD or Solana's incinerator is a
deliberate burn as the price (wafermap.xyz burns its own WAFERMAP token on Robinhood Chain), not a placeholder. It
is an info line (payment_burns_token) and leaves the verdict to the other options.
- Real payments on chain. Every hour
onchain-payments.yml reads the
new Base blocks (scripts/onchain-payments.js, lib/onchain-payments.js) and counts, per payout address the daily
scan saw (paytos.json on trust-data), the USDC payments the payer signed with EIP-3009
(transferWithAuthorization, the way x402's exact scheme settles USDC) and from how many different wallets. Plain
transfers, Permit2 and Circle Gateway batches don't count, and x402 Doctor's own test wallet is left out. The result,
8 days per address, is on the payments-data branch. The paid pre-payment check adds signals.onchain_payments
(days, payments, payers, usd, coverage: the share of the window's blocks that were read) and an info line
(onchain_payments), e.g. "47 different wallets paid this seller's payout address on Base in the last 7 days". It
never changes the verdict: new sellers start at zero. A wallet that paid 10 or more different payout addresses in the
window is a checker or scanner, not an ordinary buyer: payers_wide counts those, and the info line says when some
or all of the payers are such wallets ("no ordinary buyer seen yet"). Over two or more days the window also has
demand: the share of wallets that paid on one day only, how many paid every day, and payments per wallet per day.
TypeSafe's Jev weighs that shape (lib/jev-demand.js, with at least 10 payers and 100 payments) and the info line
says when it looks like a few operators running many wallets rather than many independent buyers (demand.judged:
few_operators, mixed or broad). GET /api/trust/payments has demand per seller, with Jev's label for the top
30 once judged. GET /api/trust/summary adds the totals over
all payout addresses (onchain_payments, with pay_tos_paid_only_wide: the addresses paid only by such wallets;
393 of 774 on 19–20 Oct), shown on /trust. Read-only, no key: BASE_RPC_URL is asked first when set, then
the public node base-rpc.publicnode.com, which serves only about the last 10,000 blocks, so a gap of more than
~5 hours between runs shows up as lower coverage. GitHub runs the hourly schedule late (every 3.5 to 6.5 hours on
19 Oct), so the Doctor starts the workflow every hour itself, with the DELIVERY_DISPATCH_TOKEN it already has.
- Real payments on Solana. The same workflow reads the USDC payout addresses on Solana the scan saw
(
paytos-solana.json, with the facilitators that pay their fees: each offer's extra.feePayer) and counts, per
address, the USDC transfers into it whose network fee one of those facilitators paid, the way x402's exact scheme
settles on Solana (lib/onchain-payments-solana.js); a plain transfer, where the sender pays its own fee, doesn't
count. Solana has no log filter over many accounts, so each address's USDC accounts are asked for their new
signatures since the last run and each new transaction is read once; addresses not reached within the run's time
budget are read by the next run, so counts come later but nothing is lost. The result, in the same shape as Base's,
is payments-solana.json on payments-data. A seller without a Base option gets the same onchain_payments line
and signal for its Solana payout address, /api/trust/summary adds onchain_payments_solana, and the preflight's
Solana options carry fee_payer. SOLANA_RPC_URL is asked before the public node, which allows a few calls a second.
- Real payments on the XRP Ledger. The same workflow reads the XRPL payout addresses the scan saw, plus every
merchant the XRPL AI Hub lists, API listed or not (
paytos-xrpl.json), and counts, per address, the RLUSD and USDC payments into it that an x402 client marked
(lib/onchain-payments-xrpl.js). On the XRP Ledger the buyer signs a complete Payment and pays its own fee, so no fee
payer tells an x402 payment apart; x402 clients mark it instead: t54's SDK signs with SourceTag 804681468, and
@x402/xrpl binds the invoice in InvoiceID. A payment with neither, or one in XRP (no dollar amount without a price),
doesn't count; RLUSD and USDC are recognised by their issuers. x402's batch-settlement scheme settles on XRPL with a
Check (t54's "xrpl-check-session"): the buyer writes one to the seller and the seller cashes it for many calls at
once, so each cashed Check counts as one payment from the buyer who wrote it. Each address's transactions since the last ledger read
are asked with account_tx, oldest first, and a run cut short keeps its last ledger whole for the next one. The
result is payments-xrpl.json on payments-data; a seller without a Base or Solana option gets the same
onchain_payments line for its XRPL payout address, and /api/trust/summary adds onchain_payments_xrpl.
Since 3 Nov 2026 each payment's mark is kept per day (markers in the file), so the totals and the list of who gets
paid split the window by client: by_marker with source_tag (t54), invoice_id (@x402/xrpl) and both, over
the days counted that way.
XRPL_RPC_URL is asked before the public nodes (xrplcluster.com, xrpl.ws).
- Real payments on Stellar and Algorand. The same workflow counts the USDC payments into the Stellar payout
accounts (
paytos-stellar.json; a Soroban transfer the payer authorized and a facilitator sent,
lib/onchain-payments-stellar.js) and the Algorand payout addresses (paytos-algorand.json; a USDC transfer in an
atomic group whose fee another transaction paid, the facilitator's fee pooling, lib/onchain-payments-algorand.js).
On both, the addresses the x402 facilitators paid for are watched too, registry listed or not: each run reads what is
new at their fee accounts (OpenZeppelin and agent402.tools on Stellar, GoPlausible's fee payer on Algorand) and keeps
an address found that way for 14 days after its last such payment (found in the file). The results are
payments-stellar.json and payments-algorand.json on payments-data; /api/trust/summary adds
onchain_payments_stellar and onchain_payments_algorand. STELLAR_HORIZON_URL and ALGORAND_INDEXER_URL are asked
before horizon.stellar.org and AlgoNode's public indexer.
- Real payments on X Layer. The same workflow counts the signed (EIP-3009) USD₮0 and USDC payments into the X Layer
payout addresses (
paytos-xlayer.json), exactly as on Base, 100 blocks per call (the most rpc.xlayer.tech serves;
lib/onchain-payments-xlayer.js). The result is payments-xlayer.json on payments-data; /api/trust/summary adds
onchain_payments_xlayer, GET /api/trust/payments?network=xlayer lists who got paid, and /xlayer shows it.
XLAYER_RPC_URL is asked before rpc.xlayer.tech.
- Real payments on Monad. The same workflow counts the signed (EIP-3009) USDC payments into the Monad payout
addresses (
paytos-monad.json), exactly as on Base, 100 blocks per call (the most rpc.monad.xyz serves; about 120
calls an hour at 0.3-second blocks; lib/onchain-payments-monad.js). The result is payments-monad.json on
payments-data; /api/trust/summary adds onchain_payments_monad and GET /api/trust/payments?network=monad lists
who got paid. MONAD_RPC_URL is asked before rpc.monad.xyz.
- Polygon and Arbitrum (since 5 Nov 2026): the same count for USDC payout addresses on Polygon (eip155:137) and
Arbitrum (eip155:42161), the two biggest networks in the Trust Index that weren't counted (443 and 396 sellers on
5 Nov). The scan writes
paytos-polygon.json and paytos-arbitrum.json; the hourly run reads the public nodes
(polygon-bor-rpc.publicnode.com, arb1.arbitrum.io; 10,000 blocks per call when the call names the USDC contract) and
writes payments-polygon.json and payments-arbitrum.json (lib/onchain-payments-evm.js). /api/trust/summary
adds onchain_payments_polygon and onchain_payments_arbitrum; GET /api/trust/payments?network=polygon (or
arbitrum) lists who got paid. POLYGON_RPC_URL and ARBITRUM_RPC_URL are asked before the public nodes.
- Avalanche and Optimism (since 5 Nov 2026): the same count on Avalanche C-Chain (eip155:43114, 62 sellers)
and Optimism (eip155:10, 46 sellers), through avalanche-c-chain-rpc.publicnode.com and
optimism-rpc.publicnode.com (10,000 blocks per call). Files
paytos-avalanche.json, paytos-optimism.json,
payments-avalanche.json and payments-optimism.json; onchain_payments_avalanche and
onchain_payments_optimism in the summary; ?network=avalanche or optimism on /api/trust/payments.
AVALANCHE_RPC_URL and OPTIMISM_RPC_URL are asked first. World Chain, Arc and HyperEVM are not counted yet: their
free nodes serve only 100 to 1,000 blocks per call, or not reliably.
- The daily scan, when GitHub skips it. The scan's schedule fires four times a day and the first run does the day,
but GitHub drops scheduled runs when it is busy (on 21 Oct the 03:17 run never started). From 04:15 UTC, every half
hour, the Doctor checks the scan it serves; while that is the day before's, it starts
trust-scan.yml with
only_if_missing, which stops at once if today's scan was published meanwhile and waits behind one still running
(lib/scan-watch.js, at most one start per two hours, the same DELIVERY_DISPATCH_TOKEN).
GET /api/trust?url=<resource> (free, 30/min per IP): the track record of one resource.
POST /api/trust/batch with {"urls": [...]} (free, up to 100 URLs, 10 calls/min per IP): the same for many at once,
in the order sent (found: false for a URL the index doesn't have), with the index's days and updated once at the
top. For directories and indexers that re-test their own lists; the whole index is on the trust-data branch.
POST /api/trust/submit with {"url": "https://…"} (free, 10 an hour per IP): a seller adds an endpoint no registry
lists yet. Doctor checks that it answers 402 with a payment challenge (x402, MPP or L402) and keeps the submission in
the usage log repo (events/trust-submit/); the next scan reads GET /api/trust/submitted and scans it into
submitted.json, apart from the index, so lookups and badges find it (marked submitted: true) while the totals,
pages and datasets keep counting registry listings only. At most 25 per host; one that a registry lists later moves to
the index, and one not payable on any of its last 14 scanned days drops out.
GET /api/trust/summary: totals of the latest scan. /trust: the public page, with lookup and the scan's rules.
GET /api/trust/network?network=xrpl (or an id like xrpl:0, free): every scanned x402 endpoint that offers a network,
payable there today or not and why (the scan records unpayable networks with their first problem), grouped per
seller site. /xrpl: the public status page of x402 on the XRP Ledger, with the ledger checks of each payout account
and who gets paid there: GET /api/trust/payments?network=xrpl (free) lists every watched XRPL payout address paid in
the last 7 complete days, most money first, with its XRPL AI Hub merchant name or its endpoints' hosts, payments,
dollars, wallets and how many of them pay ten or more sellers (the names come from the daily scan, in
paytos-xrpl.json). /algorand: the same for x402 on Algorand, with the USDC opt-in of each payout account. /stellar: the same for x402 on Stellar, with the USDC trustline of each payout account. Both list who gets paid there too (?network=algorand, ?network=stellar), naming the facilitator for an address no registry lists.
- Distinct services and their kind. Sellers often list the same API many times (once per coin, city or ticker),
so endpoint counts overstate the market. Payable endpoints of one seller whose descriptions share most of their
words (Jaccard ≥ 0.5,
lib/services.js) count as one service; on 9 Oct 2026 the 40,451 payable endpoints were
18,475 services, and one seller alone listed a third of them. Each service gets a category (crypto, security,
on-chain, AI, search, finance, media, places, social, developer tools, other), chosen by TypeSafe Jev after the
daily scan (scripts/categorize.js, cached per description in categories.json on the trust-data branch, at most
1,500 new ones a day) and guessed from keywords until then. In /api/trust/summary (services) and
/api/trust/network (services, categories, per seller services, per endpoint category and
same_service_endpoints), and on /trust, /algorand and /xrpl.
- Possible bait. After the scan, the same bait check the paid preflight runs (TypeSafe Jev: an official-looking
service of a well-known brand on a domain the brand doesn't own, or a lure with free tokens, airdrops or guaranteed
returns) goes over every service, at most 1,000 new ones a day, cached per description in
categories.json.
Services Jev is sure about (≥ 0.85) are listed on /trust and in /api/trust/summary (services.bait: checked,
flagged and the surest 100), and marked on their endpoints in /api/trust/network and on /algorand and /xrpl.
- Run it yourself:
node scripts/trust-scan.js --out trust-data --limit 200, then node scripts/trust-views.js --dir trust-data.
Weekly x402 health report
Every Monday, weekly-report.yml turns the last 7 days of the Trust Index into a
report: how many Bazaar resources are clean, payable with a caution or not payable, the most common reasons with the fix
for each, which networks payable resources accept, and what changed this week (broke, fixed, flaky, new). It names no
seller or host. The report, its numbers and drafts for X, Discord and Reddit (for a person to review and post) go to
reports/<date>.md, .json and -posts.md on the weekly-reports branch. Locally:
node scripts/weekly-report.js --out reports [--index trust-data/index.json].
The same run writes a digest per network, Algorand and the XRP Ledger (network-digest.js):
how many endpoints could be paid there, from how many sellers, what changed since last week (new endpoints, ones that
stopped being payable, new sellers), how many accept only that network, the median price and the biggest sellers. It
names payable sellers only; problems are counted. Files: reports/<date>-<network>.md and -posts.md (X and community
drafts), plus reports/networks/<network>-latest.json, the snapshot next week compares with. Locally:
node scripts/network-digest.js --out reports [--index trust-data/index.json] [--previous reports/networks].
Fizzl Buy (buy.fizzl.eu)
Fizzl's fourth agent, from this repository (buy-server.js, lib/buy/), runs as its own Render service. An agent
says what it needs and the inkoper buys the right paid API for it: POST /v1/buy {"need": "weather forecast for Amsterdam tomorrow"}, $0.02 over x402 (USDC on Base or Solana), for services up to $0.01.
- Pick (
lib/buy/search.js): the wallet server's ranked search of the whole Bazaar, kept to sellers payable in
USDC on Base within the cap and payable on at least 80% of 3 or more days x402 Doctor checked.
- Fill in (
lib/buy/fill.js): Claude answers the seller's input from the need (the keys of its Bazaar example,
path parameters into the path).
- Check and buy (
lib/buy/purchase.js): a fresh preflight must say go (OFAC payout screen, track record,
namesake, bait); then the inkoper's own wallet (BUY_WALLET_KEY, kept small) pays, within BUY_DAILY_USD a day
(default 5) and its balance.
- Check the answer: TypeSafe Jev's answer check (
lib/jev-answer.js). Not what was paid for, an error or an empty
answer moves on to the next candidate, at most three. When none delivers the answer is an error and the agent's
own payment is not settled.
GET /v1/pick?need=… and the MCP tool x402_pick (free, 10 an hour) say what would be bought and why; the MCP tool
x402_buy buys, paid inside the call ($0.02 over x402, _meta["x402/payment"], the same networks as the HTTP route, and
the same lock against one payment used twice). The MCP server
(https://buy.fizzl.eu/mcp) is in the official MCP registry as io.github.Fizzl13/fizzl-buy (server.buy.json,
published with the Publish to MCP registry workflow, server fizzl-buy). In the ERC-8004 Identity Registry on Base it
is agent #988891.
node scripts/buy-dry-run.js "<need>" … shows the picks, the filled-in requests and the preflights without paying
(needs ANTHROPIC_API_KEY). On 23 Oct 2026 it found a seller with a preflight go for all seven test needs (weather,
BTC price, web search, crypto news, EUR to USD, a token check, a translation), passing over a caution and a seller
that didn't answer 402.
One payment buys one answer: x402 verifies a payment before the purchase and settles it after, so the same payment
sent several times at once would otherwise run several purchases with the inkoper's money; a payment already in use is
refused with a 409 (lib/buy/once.js). The day budget (BUY_DAILY_USD) survives a deploy: each purchase writes what
it paid sellers into the usage log, and a new instance counts what the others spent today (lib/buy/spent-log.js).
Live at https://buy.fizzl.eu since 23 Oct 2026 (the first indexing payment was delivered in 15 seconds). Like the
other fizzl.eu services, its 402 names the seller (x402-merchant) and, with BUILDER_CODE set on the Render
service, carries the Base Builder Code.
Agent skills
Two Agent Skills in skills/ teach a coding agent (Claude Code, Codex, Cursor and others that read SKILL.md) when and how to use Doctor:
x402-preflight: before the agent pays any x402 or MPP endpoint (awal x402 pay, x402-fetch, Privy's x402 client, mppx…), it looks up the free track record, then buys the $0.001 preflight and follows go / caution / no_go, and reports the outcome afterwards.
x402-seller-check: after building or changing a paid endpoint, it runs the free check until it passes, and buys the $0.05 fix when a problem isn't obvious.
Claude Code plugin
In Claude Code, the same skills come as a plugin together with the MCP server below, and presign-guard's skill and MCP server as a second plugin. Fizzl Buy, the Fizzl Agent Wallet and Ichimoku Signal have a plugin each, from Fizzl13/fizzl-skills, the overview of every Fizzl skill:
The marketplace is .claude-plugin/marketplace.json. The paid tools of both MCP servers need an MCP client that pays x402; without one, the skills pay over HTTP with your own x402 client, as they describe.
MCP server
https://x402-doctor.fizzl.eu/mcp is an MCP server (Streamable HTTP, stateless) for Claude, Cursor and agent frameworks, listed in the official MCP registry as io.github.Fizzl13/x402-doctor. Directories can read its tools without connecting from the server card at /.well-known/mcp/server-card.json (the same list as tools/list).
https://x402-doctor.fizzl.eu/mcp/free is the same server with only the free tools (x402_quick_check, x402_payee_check, concordium_badge_check, check_payment_proof, x402_trust_record, x402_trust_add), for directories whose clients don't pay, such as Claude's connectors directory. Claude calls connectors from Anthropic's outbound range (160.79.104.0/21), so calls from there share one pool of 300 free calls an hour instead of 10 per address, on both endpoints.
The paid tools are paid inside the MCP call with the x402 MCP transport (_meta["x402/payment"]), on Base or Solana, at the same prices and to the same payout wallets as the HTTP routes. Invalid input is refused before payment, and a failed call is not charged. A delivery check ordered over MCP is queued only once its payment settled, like the HTTP route.
Verified by Concordium
When a seller's /.well-known/agent-card.json carries a Concordium badge (the A2A extension
https://docs.concordium.com/a2a-extensions/concordium-badge/v1), the web check adds concordium-agent and
x402_payee_check adds concordium (lib/concordium-agent.js). The badge is a CIS-2 token address in Concordium's
Agent Registry (CIS-8004, the Concordium form of ERC-8004). Each agent is an NFT owned by a Concordium account, and
every Concordium account belongs to a verified identity at protocol level, so there is an accountable owner behind it.
The check reads the registry through Concordium's hosted Agent Registry service (read-only). The badge's own contract
decides the network: <10082,0> is mainnet, <12802,0> is testnet, and any other contract is not trusted. Then:
- the agent must be
Active;
- the card at its on-chain
agent_uri must hash (SHA-256) to the on-chain metadata_hash;
- that anchored card must list this endpoint's origin, or be served from it. A copy of the card on another server
carries the same badge, so the badge alone proves nothing.
Then, per payment option, one outcome:
verified-wallet: a Concordium payTo is the agent wallet.
verified-external-key: a payTo on another chain (EVM, Solana) is the address of the key the agent bound with
CIS-8, and the CIS-8 registry still holds that binding for the agent's owner.
owner-only, no-agent-wallet, mismatch or not-bound.
- For the whole agent:
revoked, card-mismatch, endpoint-unlisted, or not-read. not-read is never a pass or
a fail.
Every payTo verified is a pass. A mismatch, a revoked agent, a swapped card or an unlisted endpoint is a warning with
the fix.
The paid pre-payment check (/api/v1/preflight, MCP x402_preflight) uses the same outcomes (the daily scan doesn't
ask: one more request per endpoint). A badge that vouches for the seller adds an info
line (concordium_verified) and signals.concordium. A badge that doesn't is caution: concordium_payto_mismatch
(the payout address is not the agent's; it names the networks where it is), concordium_endpoint_unlisted (a copied
badge), concordium_revoked or concordium_card_mismatch. A payout address that changed since the daily scan
(payto_changed) but is the agent's wallet or its CIS-8 key is the seller's own move: payto_changed_bound, info,
as for ERC-8004. No badge, no key for that chain, or a registry that didn't answer changes nothing.
Concordium Trusted Agents Hackathon
x402 Doctor is itself a Concordium agent: #5120 in the Agent Registry on mainnet (contract <10082,0>), badge
Mf22oVsbu3vf4Jb1cga49, registered
4 Nov 2026 by an identity-verified mainnet account. Its Agent Card is at
/.well-known/agent-card.json; the card's SHA-256 is pinned
on chain and its Base payTo 0x6B0F…5F25 is bound to the agent with CIS-8, so its own payee check says
verified-external-key on Base and X Layer.
On chain (CCDScan, mainnet):
The pin page (/concordium-agent.html) takes the badge or the
"Agent ID" that Concordium's registry app shows, and turns the latter into the badge.
It was first built and tested as agent #993 on testnet (badge JyP9Y8gHmf4U8QPMm1TkT). On chain (CCDScan, testnet):
- Registered:
minted agent #993, with the owner account as agent wallet.
- Card pinned:
setAgentURI with the card's SHA-256.
- Base key bound:
setExternalReference, CIS-8, eip155:8453.
The card now carries the mainnet badge (a card shows one badge), so #993's pinned hash no longer matches it.
node concordium/demo.mjs reads the registry live and pays nothing. It shows x402 Doctor verified, and three things
an agent must not pay, all refused:
- a copycat server with the same card and badge (
endpoint-unlisted);
- Doctor's own card with a swapped payTo (
mismatch);
- the badge of a revoked agent (
revoked).
/concordium-agent.html does the two steps Concordium's registry
app can't do after registration, for any agent, signed in the owner's own wallets. It pins the card's hash, and it binds
an EVM payTo (MetaMask signs one message, the Concordium wallet signs setExternalReference).
/concordium-proof.html lets a seller's owner prove facts about
the identity behind a Concordium payTo as a zero-knowledge proof from their own wallet (an adult, an EU resident or
national, an identity document still valid), tied to that payTo and the seller's site, and checked with Concordium's
verifier. The seller serves it at /.well-known/concordium-proof.json; the Concordium x402 facilitator in
Fizzl13/x402-examples reads it before it sponsors a payment. Nothing else about the person is revealed.
x402 Doctor's web check reads it too: when a seller serves one for a Concordium payTo in its 402, the check
concordium-proof says what is proven (pass), or why the proof doesn't hold (warn: another site or payTo, a proof that
doesn't verify with Concordium's verifier, or a credential that isn't the payTo account's own).
The same page in buyer mode (?role=buyer) makes
the other side's proof: facts about the person behind the account a paying agent pays from, tied to that account. The
agent sends it with its payments (extensions["concordium-buyer-proof"]) to sellers that ask for it
(extra.buyerProof in their 402, e.g. ["adult"]), and the facilitator checks it before it sponsors the payment.
The registry on 5 Nov 2026 (every agent and every card, read through Concordium's service):
- Mainnet has 5,121 agents from 108 owners. Every agent wallet that is set is the owner's own account.
- 2,429 agents bind a key on another chain with CIS-8 (NEAR, Ethereum, Solana, Fetch.ai, XDC, Celo, Base), most of
them in a few large fleets.
- 22 of the cards that could be fetched mention x402. Some x402 sellers bind a key on their payment chain, for example
as one of the owners of the agent's wallet on XDC.
- x402 Doctor (agent #5120) is the only one whose x402 payTo is itself the key bound to its badge.
That link, from the badge to the address a buyer is about to pay, is what this check reads. Keys bound uncompressed
or compressed are both read.
Path to production:
- The check is live today in the free web check, the free MCP tool
x402_payee_check and the paid preflight, whose
verdict uses the outcome (x402-doctor#509): payto_changed trusts a move that the agent's CIS-8 key covers, as it
does for ERC-8004. It needs no setup from sellers beyond the badge.
- x402 Doctor is registered on mainnet (agent #5120, contract
<10082,0>). The same module serves both registries.
- Offer x402 on Concordium itself (CCD or PLT), as soon as a facilitator settles it.
Feedback
Found a bug, or missing something? Send it with POST /feedback (free, no payment) or the MCP tool feedback:
type is bug, feature or other; message is required (up to 2000 characters); endpoint and contact are optional. The answer is 202 with an id. At most 10 reports per hour per caller. Reports go to the usage log (shown on /admin/usage), and a person reads every one; nothing in a report is run or changed automatically. GET /feedback shows the schema. A diagnosis never calls another server's feedback tool: tools that call themselves free are skipped.
CLI
Exit code 0 on pass/warn, 1 when a check fails (or on warnings with --strict), 2 on usage errors.
The CLI can diagnose localhost, so you can run it against a dev server.
GitHub Action
Check your x402 endpoints on every push. The step fails on a broken 402, each
problem shows up as an annotation on the run and the PR, and the job summary
lists what is wrong with the fix next to it. On the
GitHub Marketplace
as x402 Doctor check; a public endpoint that passes also gets a badge snippet.
Outputs: overall (pass, warn or fail, the worst across the URLs) and
report (the full reports as JSON). The Action uses the runner's own Node
(20.18 or newer, as on GitHub-hosted runners) and does not change the job's
Node version. Read-only: no wallet, no payment.
Or the CLI in a plain run: step:
Configuration
| USAGE_LOG_TOKEN | Fine-grained GitHub token with Contents read/write on the usage-log repo only. Every call is logged there (see below) |
| USAGE_LOG_REPO | The private usage-log repo (default Fizzl13/usage-log) |
| ADMIN_PASSWORD | Password for /admin/usage (any user name). Without it the admin pages do not exist |
Without either payout wallet the paid route answers 503 and the rest of the app works as before.
Usage dashboard
/admin/usage shows every call to the Fizzl services (x402 Doctor, Ichimoku Signal, presign-guard, Fizzl Buy):
totals, revenue, paying wallets, calls per day per service, the most asked inputs and the latest calls with what
was filled in, the result and the payment (with a Basescan/Solscan link). Each service appends one JSON line per
call to events/<service>/<YYYY-MM-DD>/<HH>.jsonl (one file per UTC hour since 25 Oct 2026; one per day,
events/<service>/<YYYY-MM-DD>.jsonl, before) in the private usage-log repo (lib/usage-log.js, copied into each
service); the dashboard reads both back (lib/usage-reader.js). A bot's unpaid 402s are one line per hour, route
and agent, with count and visitors (how many different callers). 402 challenges, health checks and static files
are not counted as calls, and inputs are cut to 300 characters. Logging never delays or fails a request.
Payment security (Five Attacks on x402)
Five Attacks on x402 (Li, Wang and Wang, 2026) describes five ways an x402 payment can go wrong between the HTTP request and the chain, and maps each to a mitigation. How this service handles them:
Doctor checks other sellers for two of them as well: the paid delivery check records whether a CDN may serve the paid answer for free (III), and the pre-payment check covers IV for every endpoint a buyer is about to pay.
Safety
The web app fetches user-submitted URLs server-side:
- Every connection is checked against private and reserved ranges (RFC 1918, loopback, link-local and cloud
metadata, CGNAT, IPv6 ULA/link-local, IPv4-mapped IPv6 in any notation) at connect time, so DNS rebinding
cannot slip past a separate lookup. Redirects are followed manually and each hop is re-checked.
- Responses are capped at 2 MB and every request times out after 8 s.
/api/diagnose (free) is rate-limited per IP; /api/v1/diagnose is paid per call and uses the same guards.
- Everything taken from the diagnosed endpoint is rendered as text, never as HTML.
It never pays the endpoint it diagnoses: no wallet, no signatures, zero financial risk. A paid probe (sign and settle one minimal payment
to trace verify/settle failures) is deliberately out of scope here.
Project structure
Tests
The suites run the doctor against local fixture servers: a healthy v2 service (Base + Solana, Bazaar, OpenAPI,
mainnet paywall) and a broken one that reproduces the real bugs above, plus SSRF, size-cap, API and CLI tests.
License
Copyright (C) 2026 Frits Zwager. Licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only); see LICENSE. If you run a modified version as a network service, you must offer its source code to the users of that service.