{"id":"ac044492-7cbc-4f73-889a-7dbf47ef96de","slug":"x402-doctor","kind":"mcp","name":"x402-doctor","summary":"a tool to run diagnostics for x402","owner":"Fizzl13","repoUrl":"https://github.com/Fizzl13/x402-doctor","license":"AGPL-3.0","licenseTier":"strong-copyleft","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-09-21T12:47:35.000Z","addedAt":"2026-10-14T05:12:13.277Z","logoUrl":"https://avatars.githubusercontent.com/u/49376692?u=34cf1ba24ab31ade0aaf792ac6cdede706ce4314&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["utilities"],"tags":["x402","x402-api","x402-payments","x402-protocol","x402-services","x402protocol","web3","ai-agents"],"runtime":"remote","local":false,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-17T13:25:56.691Z","sample":false,"url":"https://conduits.dev/tools/x402-doctor","language":"JavaScript","transports":["http"],"install":{"remoteUrl":"https://x402-doctor.fizzl.eu/mcp"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# x402 Doctor\n\n[![x402 payable](https://x402-doctor.fizzl.eu/badge.svg?url=https%3A%2F%2Fx402-doctor.fizzl.eu%2Fapi%2Fv1%2Fdiagnose)](https://x402-doctor.fizzl.eu/trust?url=https%3A%2F%2Fx402-doctor.fizzl.eu%2Fapi%2Fv1%2Fdiagnose) [![Listed on x402-list](https://x402-list.com/badge/x402-doctor.svg)](https://x402-list.com/services/x402-doctor?utm_source=badge&utm_medium=referral&utm_campaign=embed) · [Live status](https://x402-doctor.fizzl.eu/status) · [![x402 Doctor MCP connector on Glama](https://glama.ai/mcp/connectors/io.github.Fizzl13/x402-doctor/badges/score.svg)](https://glama.ai/mcp/connectors/io.github.Fizzl13/x402-doctor)\n\nDiagnoses why an x402-payable endpoint's payment flow is broken, without needing a funded wallet.\n\nPaste a URL (web app) or run `x402-doctor <url>` (CLI, CI) and get back exactly which check failed, why, and how\nto fix it. Every check traces back to a real bug hit while shipping\n[PlainText](https://plaintext.fizzl.eu) and\n[Ichimoku Signal](https://ichimoku-signal.fizzl.eu).\n\n[![x402 Doctor in 70 seconds](https://raw.githubusercontent.com/Fizzl13/x402-doctor/explainer-video/poster.jpg)](https://x402-doctor.fizzl.eu/media/explainer.mp4)\n\n▶ **[Watch the 70-second explainer](https://x402-doctor.fizzl.eu/media/explainer.mp4)** (with voice and captions):\ndiagnosing a broken endpoint, the pre-payment check for agents, and the daily Trust Index.\n\n[![x402 Doctor: the paid fix](https://raw.githubusercontent.com/Fizzl13/x402-doctor/fix-video/poster.jpg)](https://x402-doctor.fizzl.eu/media/fix.mp4)\n\n▶ **New: [the paid fix in 70 seconds](https://x402-doctor.fizzl.eu/media/fix.mp4)**: for $0.05 the Doctor hands you\nthe medicine, the exact code that fixes each problem for your stack, in the browser or as JSON for agents.\n\n## What it checks\n\n| Group | Check | Catches |\n|-------|-------|---------|\n| 402 challenge | `returns-402` | No 402 for GET or POST (tries both, or `--method`). Behind Cloudflare or Amazon CloudFront the hint names the usual Monetization Gateway or AWS WAF Monetize rule misses. An endpoint that asks for x401 proof first is info, not a failure |\n| | `x401` | An x401 proof request (`PROOF-REQUEST`, [x401.id](https://x401.id)) instead of a 402: the endpoint asks who is behind the agent before it asks for payment. Pass when the base64url payload names scheme `x401`, a version and credential requests (with their protocols, the token endpoint and the payment hint); a warning when agents can't read it; info for the older header name `PROOF-REQUIRED` |\n| | `protocol-version`, `challenge-header` | v1 vs v2, missing `x402Version`, challenge in the wrong header, undecodable header |\n| | `envelope-body-mirror` | Header-only challenge (`@x402/express` sends `{}`), header and body disagreeing |\n| MCP (payment per tool call) | `mcp-server` | When the URL answers no 402 but speaks MCP (Streamable HTTP, JSON or SSE, with or without a session): initialize and `tools/list` without payment |\n| | `mcp-paid-tools` | No tool description says it is paid (x402, USDC, a price), so agents can't tell |\n| | `mcp-payment-required` | One unpaid `tools/call` per paid-looking tool (at most 2; explicitly paid tools first — \"Paid …\" or a price / \"via x402\" in the title; never tools that call themselves free, destructive tools, or write tools unless explicitly paid). A free answer warns only for an explicitly paid tool, otherwise it is info: the x402 payment requirement in the tool result with `isError: true` (as `@x402/mcp`), or an HTTP 402 with `PAYMENT-REQUIRED` instead (payable over HTTP, not by `@x402/mcp` clients), a JSON-RPC error, a tool that answers for free, or example arguments refused before the payment step (publish one valid call as `_meta.examples` to avoid that). The requirement then gets the payment-option, settlement and wallet checks below |\n| | `mcp-tool-poisoning` | Hidden orders for the agent in tool descriptions (tool poisoning): hidden Unicode characters, `<IMPORTANT>` blocks, reading `~/.ssh` or `.env`, \"do not tell the user\", sending the maximum USDC (code rules), then TypeSafe Jev per tool (with `TYPESAFE_API_KEY`). A sure finding is `warn`, an unsure one `info`. Calibrated on 755 tools of 96 servers from the official MCP registry: no false alarm, and five made-up poisoned tools all caught |\n| | `mcp-payment-text`, `mcp-payment-structured` | The requirement only in `structuredContent` (the x402 MCP transport requires JSON text in `content[0]`), or only as text (structuredContent preferred) |\n| Payment options | `accepts[i]-scheme`, `-network` | Missing scheme; legacy names like `base` / `solana:mainnet` instead of CAIP-2 |\n| | `accepts[i]-payto`, `-asset` | Invalid EVM, Solana, XRPL or Algorand addresses (XRPL and Algorand with their checksums); asset that is not USDC, or USDC of another network (e.g. Base Sepolia USDC on Base); on Algorand an asset that is not an ASA id (USDC is ASA 31566704); on XRPL an XRP amount that isn't in drops, or \"RLUSD\" from another issuer than Ripple's |\n| | `accepts[i]-amount` | Decimal dollar amounts (`\"0.02\"`) instead of atomic units, zero or non-integer amounts |\n| | Stellar options | x402 `exact` on Stellar (`stellar:pubnet`, `@x402/stellar`): payTo a G…, C… or M… address with a valid StrKey checksum, asset a Soroban token contract (USDC is `CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75`; `\"USDC\"` or a testnet contract fails), a positive integer amount (USDC has 7 decimals: `100000` is $0.01) and `extra.areFeesSponsored: true`, without which the client refuses |\n| | `aws-test-mode` | A 402 served through Amazon CloudFront that offers only testnets (info): AWS WAF Monetize left in Test currency mode |\n| | `accepts[i]-extra` | Solana without `extra.feePayer` (clients throw \"feePayer is required\"), EVM without the EIP-712 `name`/`version` |\n| | `accepts[i]-batch`, `-batch-delay`, `-batch-deposit`, `batch-escrow` | The `batch-settlement` scheme (one deposit into the x402 escrow, then a signed voucher per call, claimed in batches): `extra.receiverAuthorizer` missing (clients refuse), `withdrawDelay` missing or outside 900 s to 30 days, no EIP-712 domain for the EIP-3009 deposit, `minDeposit` not in atomic units, the escrow not deployed on the network; the deposit and withdraw delay buyers commit to |\n| | `accepts[i]-extra`, `circle-gateway`, `accepts-order` | Circle Gateway options (\"exact\" signed for the Gateway Wallet, domain `GatewayWalletBatched`): `extra.version` other than \"1\" or no `verifyingContract` (Circle's client skips or can't sign it), a contract that isn't Circle's Gateway Wallet; who can pay them (a Gateway balance and Circle's client, not standard x402 clients or browser wallets; their 7-day timeout is right); a Gateway option listed before the standard one on the same network, which x402 clients would pick and fail |\n| Resource | `resource-url` | `http://` resource URL on an `https://` endpoint (Express behind a TLS proxy without `trust proxy`) |\n| | `resource-metadata` | Missing description / mimeType |\n| Settlement | `solana-payout-account` | Solana payout wallet without a token account for the asset: every settlement fails on-chain |\n| | `xrpl-payout-account`, `xrpl-payout-trustline` | XRPL payout account that was never activated, has Deposit Authorization on, or requires a destination tag the option doesn't give; for a token like RLUSD, no (or a frozen) trust line to its issuer: every payment fails |\n| | `accepts[i]-lightning`, `lightning-invoice`, `lightning-request-hash` | Bitcoin Lightning (x402 `exact` on `lnbtc`, Sept 2026): payTo must be the node key, amount in millisatoshis, `paymentFlow: \"upfront\"` and a request binding (`http:1`/`mcp:1`); the BOLT11 invoice in `extra.invoice` is decoded and must match the option exactly (amount, `bc`/`tb`, expiry = `maxTimeoutSeconds`, signed by payTo, not expired) and commit to `extra.requestHash`, which for `http:1` is recomputed for the doctor's own request |\n| | `algorand-payout-optin` | Algorand payout address that has not opted in to the asset (an account can only receive an ASA after opting in): every payment fails |\n| | `stellar-payout-trustline` | Stellar payout account (G…) that was never funded, or has no (or an unauthorised) trustline to Circle's USDC: every payment fails. Read from Horizon; a contract (C…) needs none |\n| | `solana-wallets` | Solana settled by PayAI, which rejects Phantom (Lighthouse instructions before the transfer) |\n| Who can pay | `wallets` | Per wallet (MetaMask, Coinbase Wallet, Rabby, Phantom, Solflare, Backpack, Pera, Defly, Xaman, Crossmark, GemWallet, x402 agents, which also pay Stellar): the networks where payment works and where it fails, and why. Also in the JSON report as `wallets` |\n| | `metamask-site-scan` | Only with `METAMASK_SCAN=on` (off by default: the endpoint has no published API or licence). MetaMask's site scanner (Blockaid) blocks the domain (e.g. \"wallet drainer\"): MetaMask users can't open the site or pay in the browser. With how to report a false positive. Agents are not affected |\n| | `evm-payto-eoa` | EVM payout wallet is a regular wallet (EOA): MetaMask's Blockaid check may flag the payment signature as \"a deceptive request\". Info, with how to get it cleared |\n| | `coinbase-agents` | Whether Coinbase's agent wallets can pay: Wallet MCP pays x402 in USDC on Base with `exact`, `upto` or `auth-capture` and finds services through the CDP Bazaar (the line says whether this route is listed there, from `bazaar-listing`); the Coinbase MCP also needs x402 v2 and at most $5 a call, and fetches only its own curated catalog. Info when there is no USDC on Base or only another scheme |\n| Discovery | `bazaar`, `bazaar-output` | Missing or invalid Bazaar declaration, output example not matching its schema |\n| | `market-price` | Where your price sits among comparable paid APIs of other sellers in the x402 Trust Index (median and range; comparable as judged by TypeSafe Jev, with `TYPESAFE_API_KEY`). Always `info`; the hint speaks up when agents' preflights would point at cheaper services, or when you charge far less than the rest |\n| | `bazaar-replay` | The declared example request not answering 402, so the Bazaar cannot index it |\n| | `openapi-present`, `openapi-title`, `openapi-guidance` | Missing `/openapi.json`, `info.title`, `info.x-guidance` |\n| | `openapi-contact`, `openapi-ownership`, `openapi-input-schema`, `openapi-free-routes` | What x402scan and AgentCash read beyond that: `info.contact.email` (info), an ownership proof under `x-agentcash-provenance.ownershipProofs`, checked by recovering the signer of the bare origin against the EVM `payTo` (pass, or a warning for a trailing slash, another signer, a broken signature or a top-level `ownershipProofs` that x402scan ignores), paid operations without `parameters` or `requestBody` (x402scan does not register them) and operations with neither `x-payment-info` nor `\"security\": []` (free ones are reported as unprotected). Info lines unless a proof is present but wrong |\n| | `well-known` | `/.well-known/x402` missing (info), unreadable (resources as URLs or objects with a `url` are both read), or listing resources on another host than the one checked (built from the request `Host` header, it keeps pointing crawlers at an old domain). A platform address such as `*.onrender.com` pointing at its own domain is fine |\n| | `well-known-names` | Resources in `/.well-known/x402` without a `name` and `description` (info): directories such as the XRPL AI Hub list each resource by them. |\n| | `agent-instructions` | `/skill.md` on the origin, so a user can tell an agent `connect to <host>/skill.md` (Coinbase's pattern); `/llms.txt` counts as the older form, and an HTML page served on every path does not count. Info when there is none |\n| | `bazaar-listing` | Where the CDP Bazaar lists this route (same path, same `payTo`): under this origin, not yet, or only under another host. The Bazaar keeps the URL the payer used, so after a domain move a route stays listed under the old host until someone pays through the new one. On the web check and the paid API (not the CLI) |\n| | `bazaar-last-payment` | When the CDP Bazaar lists the route here: how long ago it counted the last settled payment. From 21 days a warning, since the Bazaar drops a route after 30 days without one (one paid call keeps it listed). |\n| | `cdp-validate` | Coinbase's own verdict: Doctor asks CDP's free validate API whether the CDP facilitator would index the route in the Bazaar (the index Coinbase's agents search), and shows the failures it names, without the ones it skips after the first. A warning when the endpoint offers Base or Solana, info otherwise; a pass may carry CDP's advice. HTTPS only; nothing when CDP doesn't answer. Answers cached 10 minutes. On the web check, the paid diagnose and fix, and MCP (not the CLI); the daily health check also runs it on every Fizzl paid route. `CDP_VALIDATE=off` turns it off |\n| | `merchant` | The `x402-merchant` extension (name, website, logo, categories) that GoPlausible's catalog shows on the seller: a warning when malformed, an info line when it is missing on an endpoint that offers Algorand |\n| | `builder-code` | The Base Builder Code (ERC-8021, the x402 `builder-code` extension): the buyer's client echoes the app code and the facilitator writes it into the settlement, so Base attributes the payment. A warning when the facilitator would drop it (code not under `info`, an invalid or missing `info.a`, invalid or too many service codes, an x402 v1 challenge); an info line when a route that settles on Base has none, or when no option settles on an EVM chain |\n| Browser | `paywall` | Browser paywall in testnet mode on a mainnet endpoint (`@x402/paywall` defaults to testnet; the flag is read from JS or JSON config) |\n\nStatuses: `pass`, `warn` (works, but something is off), `fail` (payments fail or it is not valid x402), `info`.\n\n## Web app\n\n```bash\nnpm install\nnpm start          # http://localhost:3001\n```\n\nReports are shareable: `https://<host>/?url=<endpoint>&method=GET` runs the diagnosis on load.\n\nAPI: `POST /api/diagnose` with `{ \"url\": \"...\", \"method\": \"GET\" | \"POST\" }` (method optional) returns\n`{ url, method, overall, checks[], challenge, share_url }`: `share_url` is the page link above, to paste into an issue or a chat. Rate-limited to 10 diagnoses per minute per IP.\n\n## Paid API for agents (x402)\n\n`GET /api/v1/diagnose?url=<endpoint>&method=GET|POST` returns the same report, **$0.01 USDC per call** on Base or\nSolana via x402, with no rate limit. It is meant for agents and CI pipelines; the web page stays free.\n\n- Unpaid requests get the x402 challenge (`PAYMENT-REQUIRED` header, mirrored in the body) with a Bazaar input and\n  output schema.\n- Invalid input (bad URL, non-http(s), unknown method) gets a 400 **before** payment. The bare route without `url`\n  answers 402, so indexers such as x402scan can register it. Paying without a `url` returns a 400.\n- The x402 middleware settles only after a 2xx response, so a diagnosis that errors out is never charged.\n- Discovery: `/openapi.json` (with `x-payment-info`) and `/.well-known/x402`.\n- Browsers opening the route get a wallet paywall (Base first: MetaMask, Coinbase Wallet) instead of the bare 402.\n- Bazaar: a facilitator lists a route after the first payment it settles for it. With the CDP keys set, one paid\n  call (e.g. from the browser paywall) puts the route in the CDP Bazaar that agents search.\n\n```bash\n# pay-per-call from Node with @x402/fetch\nconst paidFetch = wrapFetchWithPayment(fetch, client);   // client with an EVM or SVM signer\nconst report = await (await paidFetch(\"https://<host>/api/v1/diagnose?url=https://api.example.com/paid\")).json();\n```\n\n### Pre-payment check for buyers: `GET /api/v1/preflight`\n\n`GET /api/v1/preflight?url=<endpoint>&max_usd=0.05&network=<caip2>&method=GET|POST`, **$0.001 USDC per call**:\ncall it before your agent pays an x402 endpoint it has not used before. It never pays the endpoint.\n\n```json\n{\n  \"verdict\": \"go\",                     // go | caution | no_go\n  \"safe_to_pay\": true,\n  \"summary\": \"OK to pay: $0.02 on Solana.\",\n  \"recommended_option\": 1,             // index into the endpoint's accepts[]\n  \"options\": [{ \"network\": \"eip155:8453\", \"asset_symbol\": \"USDC\", \"usd\": 0.02, \"payable\": true, \"problems\": [] }, …],\n  \"signals\": { \"https\": true, \"advertised_price_usd\": 0.02, \"listed_in_cdp_bazaar\": true },\n  \"reasons\": [],                       // [{ level: no_go | caution | info, code, message }]\n  \"cached\": false\n}\n```\n\n| Verdict | When |\n|---|---|\n| `no_go` | No 402 or no valid challenge; every option would fail to settle (bad payTo/amount, missing fee payer or EIP-712 domain, Solana payout wallet without a token account); cheapest option above `max_usd`; no payable option on the requested `network` |\n| `caution` | Charges more than its OpenAPI advertises; not HTTPS; not a known USDC (or BNB Chain dollar token) contract; decimal amount, or a 6-decimal amount on an 18-decimal token; resource URL differs from the requested URL; testnet only; bait (below); 20x or more above comparable services (below) |\n| `go` | None of the above. `recommended_option` is the cheapest payable USDC option (on `network` if given); at the same price, one every x402 client can pay (EIP-3009, not Permit2) and USDC before other dollar tokens |\n\n**BNB Chain** (`eip155:56`) has no Circle USDC. Sellers price in 18-decimal dollar tokens: Binance-Peg USDC, USDT,\nUSD1 and United Stables (U), so $0.01 is `\"10000000000000000\"`. Doctor prices those options like USDC. USDC and USDT\nthere have no EIP-3009, so an option without `assetTransferMethod: \"permit2\"` fails; a value standard x402 clients\ndon't know is a warning, because `@x402/evm` then signs EIP-3009 instead. `\"permit2-exact\"` and `\"permit2-upto\"` are the\nmethods of Binance's B402 facilitator, so only its own clients pay those options.\n\n**Bait check** (TypeSafe's Jev, when `TYPESAFE_API_KEY` is set): for an endpoint that could be paid, Jev judges\nwhat the protocol checks can't. Does it claim to be, or to be official from or endorsed by, a brand that doesn't own\nits domain (`brand_impersonation`; reselling or proxying a brand's product under the seller's own name, such as an\nLLM gateway naming the models it serves, is not), lure with free tokens, airdrops or guaranteed returns (`lure_description`), or\npromise something its declared output doesn't deliver (`output_mismatch`)? A clear yes is a `caution`, an\nin-between answer `info`; it never makes a verdict `no_go` and never turns one into `go`. The probabilities are\nin `signals.jev_lure`. On 300 real CDP Bazaar listings it raised no caution for brand or lure wording. When a bait\nquestion changes (`BAIT_VERSION` in `lib/jev-lure.js`), the Trust Index's next run asks again, first, every service\nthe old wording flagged or was unsure about; clear answers carry over.\n\n**Price check** (same key): is the price far above what comparable services ask? Doctor takes up to 24 paid APIs\nof other sellers from the x402 Trust Index whose descriptions share rare words with this one (at most two per\nseller), and Jev judges per candidate whether a buyer would be served just as well by it. With at least 5 such\nservices, their median price is the comparison: 5x or more (and at least $0.05 more) is `info`, 20x or more (and\nat least $0.25 more) a `caution`, both `price_high` and naming up to three cheaper alternatives. The comparison is\nin `signals.price_check` (`comparable_median_usd`, `comparables`, `ratio`, `cheaper[]`). It never makes a verdict\n`no_go`: the price is the seller's choice, the agent decides whether it is worth it.\n\nNot being listed in the CDP Bazaar is reported as `info` only. Results are cached for 10 minutes per URL, budget\nand network (`cached: true`), so checking before every payment stays fast.\n\n### Several endpoints at once: `GET /api/v1/preflight/batch`\n\n`GET /api/v1/preflight/batch?url=<a>&url=<b>&max_usd=0.05`, **$0.005 USDC per call** for up to 10 endpoints (one\n`url` parameter each; duplicates count once). For marketplaces, directories and agents that weigh several services.\nEach endpoint gets the same verdict as a single preflight; one that errors or does not answer within 20 seconds is\n`unknown` and does not hold up the rest.\n\n```json\n{\n  \"count\": 2,\n  \"counts\": { \"go\": 1, \"caution\": 0, \"no_go\": 1, \"unknown\": 0 },\n  \"results\": [\n    { \"url\": \"https://a.example/paid\", \"verdict\": \"go\", \"safe_to_pay\": true, \"summary\": \"OK to pay: $0.02 on Base.\", \"recommended_option\": 0, \"options\": […], \"reasons\": [] },\n    { \"url\": \"https://b.example/paid\", \"verdict\": \"no_go\", \"safe_to_pay\": false, \"summary\": \"Do not pay: …\", … }\n  ]\n}\n```\n\n### Before an expensive call: `GET /api/v1/preflight/deep`\n\n`GET /api/v1/preflight/deep?url=<endpoint>&max_usd=0.5`, **$0.01 USDC per call**: everything the preflight answers,\nplus\n\n- `diagnosis`: the full diagnosis (`overall`, every check, and the failing or warning ones as `problems`);\n- `history`: the daily scans of the last 30 days, one `{ date, status }` per day;\n- `seller`: the seller's other scanned endpoints on the same origin (how many, payable today, average payable ratio,\n  how many were unreliable). A seller whose endpoints were payable on less than half of the scans turns `go` into\n  `caution` (`unreliable_seller`);\n- `domain`: the domain-move checks (`well-known`, `bazaar-listing`), e.g. a Bazaar listing only under an old domain.\n\n### Paid and delivered? `GET /api/v1/delivery-check`\n\n`GET /api/v1/delivery-check?url=<endpoint>&method=GET|POST`, **$0.25 USDC per check**: x402 Doctor buys the endpoint\nonce, with the example input its own listing declares, and publishes what came back. For sellers who want proof that\ntheir endpoint delivers, and for buyers before a large order.\n\n- **Checked before you pay**: the endpoint must be https, not a fizzl.eu route, get a `go` from the pre-payment check,\n  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\n  taken), and nothing is charged. A probe without a payment gets at most 5 seconds of this check before its 402\n  (the CDP Bazaar's validator and health probe give up after 10); a request with a payment always waits for the\n  whole check before anything settles.\n- **The answer comes at once, the purchase shortly after**: the paid call answers with a job `id` and a `status_url`.\n  [`delivery-check.yml`](.github/workflows/delivery-check.yml) takes the queue (`GET /api/v1/delivery-check/queue`,\n  free) and buys with the dedicated spot-buy wallet (`SPOT_BUY_KEY`; no wallet key on the server). Only a job whose\n  payment settled is released to the queue. The server starts the workflow as soon as a payment settles\n  (`DELIVERY_DISPATCH_TOKEN`: a fine-grained GitHub token for this repository with only Actions read and write;\n  `lib/delivery-dispatch.js`); its schedule, every 10 minutes, is the fallback, since GitHub runs scheduled\n  workflows late or not at all.\n- **The same steps as the spot-buy** (`lib/spot-buy.js`): a fresh preflight with the OFAC payout screen right before\n  paying, the seller's declared example input, the answer checked against the declared output schema, and the charge\n  measured on chain.\n- **Public result**: `GET /api/v1/delivery-check/<id>` (free) shows `queued`, then `done` with the result\n  (`delivered`, `off_schema`, `empty`, `charged_not_delivered`, `payment_rejected`, `error_not_charged`, or the step\n  that stopped it before paying) and the transaction. Every result is on the `delivery-data` branch.\n- **Caching**: for a delivered GET answer, `cache` says whether a shared cache (a CDN) may store the paid answer\n  and serve it to callers who did not pay (`shared_cache`: `yes`, `no` or `unspecified`, with the `Cache-Control`\n  it sent). The x402 payment travels in its own header, not in `Authorization`, so only `Cache-Control: private` or\n  `no-store` keeps it out ([Five Attacks on x402](https://arxiv.org/abs/2605.11781), attack III).\n\nJobs are kept in the credits Redis (`CREDITS_REDIS_URL`); without it the route answers 503 before payment.\n\n### Prepaid credits: `GET /api/v1/credits/1000`\n\nAgents 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.\n\n### The fix, as code: `GET /api/v1/fix`\n\n`GET /api/v1/fix?url=<endpoint>&method=GET|POST&stack=<optional>`, **$0.05 USDC per call**: diagnoses the endpoint,\nthen returns, per failed or warning check, the concrete change that fixes it, as code for your stack and filled in\nwith your own values (payTo, amount, network, route).\n\n```json\n{\n  \"stack\": { \"id\": \"express\", \"name\": \"Express (@x402/express)\", \"detected_from\": \"x-powered-by: Express\" },\n  \"summary\": \"3 fixes (1 blocking), for Express (@x402/express).\",\n  \"fixes\": [\n    {\n      \"recipe\": \"amount\",\n      \"severity\": \"warn\",\n      \"checks\": [\"accepts[0]-amount\"],\n      \"title\": \"Amount \\\"0.01\\\" → \\\"10000\\\" ($0.01)\",\n      \"why\": \"x402 amounts are integer strings in the token's smallest unit; USDC has 6 decimals.\",\n      \"steps\": [\"Easiest: let the SDK convert it by using price: \\\"$0.01\\\" instead of an amount.\"],\n      \"code\": [{ \"language\": \"javascript\", \"stack\": \"node\", \"label\": \"Route config\", \"snippet\": \"accepts: [{ scheme: \\\"exact\\\", price: \\\"$0.01\\\", ... }]\" }],\n      \"verify\": \"Run x402 Doctor again on https://…; accepts[0]-amount should pass.\"\n    }\n  ],\n  \"unfixed\": []\n}\n```\n\nThe stack is read from the response headers (`x-powered-by`, `server`, Vercel/Render/Cloudflare headers) and the\nchallenge: `express`, `next`, `hono`, `node` (an @x402 SDK behind another framework), `python` or `generic` (raw\nHTTP, for any language). Pass `stack=` when the guess is wrong. Node snippets use the @x402 v2 route config, which is\nthe same for @x402/express, /next and /hono; lines that only apply to Express say so.\n\nRecipes (`lib/recipes.js`, one test each in `test/recipes.test.js`): no 402 at all (200 before the paywall, auth or\nvalidation first, 404/405, unreachable), v1 or invalid challenge, wrong header name, no body mirror, no accepts,\nscheme, legacy network names, invalid payTo (spots quotes, spaces, the wrong chain), USDC asset, decimal amounts,\nSolana fee payer, EIP-712 domain, http resource URL behind a proxy, resource URL and metadata, Bazaar declaration and\nexample, Solana payout token account, Phantom on PayAI, testnet paywall on mainnet, and openapi.json. Anything\nwithout a recipe is returned under `unfixed` with the diagnosis hint.\n\n### Learning from outcomes: `POST /api/v1/outcome`\n\nAfter 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](https://www.npmjs.com/package/x402-safe-fetch) does it with `shareOutcomes: true`.\n\n```json\n{ \"outcome\": \"paid_failed\", \"status\": 402, \"preflight\": { …the signed preflight answer… }, \"query\": { \"url\": \"…\", \"method\": \"GET\", \"max_usd\": \"0.05\", \"network\": \"eip155:8453\" } }\n```\n\n- 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.\n- 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.\n- Reports land in the usage log (endpoint, outcome, status, verdict, payer from the receipt); nothing else about the buyer.\n\n### Signed verdicts\n\nEvery 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:\n\n```json\n\"receipt\": {\n  \"request_id\": \"…\", \"route\": \"GET /api/v1/preflight\", \"input_sha256\": \"…\",\n  \"payment\": { \"network\": \"eip155:8453\", \"amount\": \"1000\", \"payer\": \"0x…\", \"nonce\": \"0x…\", \"proof\": \"eip3009\" },\n  \"signed_at\": \"2026-09-27T08:00:00.000Z\", \"signer\": \"0x…\",\n  \"algorithm\": \"eip191-canonical-json-v1\", \"signature\": \"0x…\"\n}\n```\n\n- **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`](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.\n- **`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.\n- **`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>`).\n- **Signer:** [`/.well-known/x402-doctor-signer.json`](https://x402-doctor.fizzl.eu/.well-known/x402-doctor-signer.json), with retired signers listed so old receipts keep verifying.\n- **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).\n\n**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.\n\nSame format as [presign-guard](https://github.com/Fizzl13/presign-guard#signed-verdicts), so one verifier works for both. The key comes from `RECEIPT_SIGNER_SECRET` (holds no funds); without it answers are unsigned.\n\n### x402 Trust Index\n\nOnce a day, [`trust-scan.yml`](.github/workflows/trust-scan.yml) runs the pre-payment check against every resource in\nthe CDP Bazaar, the MPP directory, the [GoPlausible](https://facilitator.goplausible.xyz/discovery/resources) facilitator's discovery list (x402 on Algorand, about 2,500 endpoints the Bazaar doesn't list) and [Circle's Agent Marketplace](https://agents.circle.com/services) catalog (about 2,800 endpoints, many paid through Circle Gateway) (read-only: it stops at the 402 challenge, at most two requests per host at a time, one\n`/openapi.json` per origin, a User-Agent that links to `/trust`). The results roll into a 30-day history per resource,\none letter per day (`g` go, `c` caution, `n` no-go, `x` unreachable, `-` not scanned), published as\n`index.json` + `summary.json` on the `trust-data` branch.\n\n- The pre-payment check adds `signals.track_record` and a `caution` (`unreliable_history`) when a seller was\n  payable on fewer than half of at least 3 scanned days.\n- **Spot-buy sample: does \"go\" mean paid and delivered?** The scan only reads the 402 challenge. Once a week\n  [`spot-buy.yml`](.github/workflows/spot-buy.yml) buys a small random sample of \"go\" endpoints (`lib/spot-buy.js`):\n  x402 `exact` in USDC on Base only, one endpoint per host, never a fizzl.eu or test host, the seller's own declared\n  example as the request (when it reaches the paywall), a fresh preflight with the OFAC payout screen right before\n  paying, and hard caps (default 30 endpoints, $0.05 per call, $2 per run; at most 50, $0.10 and $5). Whether money\n  left the wallet is measured on chain (its USDC balance before and after), and the seller's `PAYMENT-RESPONSE` is\n  checked against the chain. Each result is `delivered`, `off_schema` (an answer that doesn't match the declared\n  output schema), `empty`, `charged_not_delivered`, `payment_rejected`, `error_not_charged` or `payment_failed`;\n  the run is appended to the `spot-data` branch (`latest.json`, `results/<date>.json`). It pays only with the secret\n  `SPOT_BUY_KEY`, a dedicated wallet holding a few USDC; without it every run is a dry run that pays nothing.\n  The latest run is on `/trust` (\"Paid and delivered?\") and at `GET /api/trust/spot` (summary and every purchase, no\n  wallet details); `GET /api/trust?url=…` adds `spot_buy` when that endpoint was bought in it (`lib/spot-results.js`).\n  The paid pre-payment check adds the same as `signals.spot_buy`, and a `caution` (`charged_without_answer`) when the\n  endpoint took the payment for the example request its own listing declares and answered with an error.\n  The same wallet makes one other kind of payment, started by hand: [`index-payment.yml`](.github/workflows/index-payment.yml)\n  pays our own fizzl.eu routes (`scripts/index-payment.js`), because the CDP Bazaar lists a route only after a payment\n  for it settles and drops it after 30 days without one. `stale` (the default) pays each of our 16 routes whose last\n  settled payment in the Bazaar is 21 days old or more (read from its public search), each at most its own price\n  (`INDEX_PAYMENT.routes`); `url` pays one route once. The money goes to our own payout wallets. It publishes nothing,\n  so the sample stays one of other sellers. Each run is also a self-audit of those routes: the same payment header is\n  sent once more and must get a 402 (\"Five Attacks on x402\", attack II; a route that serves it again fails the run),\n  and the settlement transaction's ERC-8021 suffix shows whether our Base Builder Code was written into it. Other\n  sellers' endpoints are never replayed.\n- **Sanctions screen.** Every payout address (x402 `payTo`, MPP `recipient`) is matched against the OFAC SDN list's\n  digital currency addresses, from [presign-guard's free open copy](https://presign-guard.fizzl.eu/v1/sanctions) of the\n  US Treasury list (`lib/sanctions.js`, refreshed every 6 hours). A listed address makes the pre-payment check `no_go`\n  (`payto_sanctioned`, with the SDN entry), in the paid check and in the daily scan; `signals.sanctions` says what was\n  screened and against which publication. `summary.json` counts the scanned resources that pay out to a listed address\n  (`payto_sanctioned`). Without the list nothing is judged (`signals.sanctions.unavailable`).\n- `GET /api/trust?url=<resource>` (free, 30/min per IP): the track record of one resource.\n  `POST /api/trust/batch` with `{\"urls\": [...]}` (free, up to 100 URLs, 10 calls/min per IP): the same for many at once,\n  in the order sent (`found: false` for a URL the index doesn't have), with the index's `days` and `updated` once at the\n  top. For directories and indexers that re-test their own lists; the whole index is on the `trust-data` branch.\n  `GET /api/trust/summary`: totals of the latest scan. `/trust`: the public page, with lookup and the scan's rules.\n- `GET /api/trust/network?network=xrpl` (or an id like `xrpl:0`, free): every scanned x402 endpoint that offers a network,\n  payable there today or not and why (the scan records `unpayable` networks with their first problem), grouped per\n  seller site. `/xrpl`: the public status page of x402 on the XRP Ledger, with the ledger checks of each payout account. `/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.\n- **Distinct services and their kind.** Sellers often list the same API many times (once per coin, city or ticker),\n  so endpoint counts overstate the market. Payable endpoints of one seller whose descriptions share most of their\n  words (Jaccard ≥ 0.5, `lib/services.js`) count as one service; on 9 Oct 2026 the 40,451 payable endpoints were\n  18,475 services, and one seller alone listed a third of them. Each service gets a category (crypto, security,\n  on-chain, AI, search, finance, media, places, social, developer tools, other), chosen by TypeSafe Jev after the\n  daily scan (`scripts/categorize.js`, cached per description in `categories.json` on the trust-data branch, at most\n  1,500 new ones a day) and guessed from keywords until then. In `/api/trust/summary` (`services`) and\n  `/api/trust/network` (`services`, `categories`, per seller `services`, per endpoint `category` and\n  `same_service_endpoints`), and on `/trust`, `/algorand` and `/xrpl`.\n- **Possible bait.** After the scan, the same bait check the paid preflight runs (TypeSafe Jev: an official-looking\n  service of a well-known brand on a domain the brand doesn't own, or a lure with free tokens, airdrops or guaranteed\n  returns) goes over every service, at most 1,000 new ones a day, cached per description in `categories.json`.\n  Services Jev is sure about (≥ 0.85) are listed on `/trust` and in `/api/trust/summary` (`services.bait`: checked,\n  flagged and the surest 100), and marked on their endpoints in `/api/trust/network` and on `/algorand` and `/xrpl`.\n- Run it yourself: `node scripts/trust-scan.js --out trust-data --limit 200`.\n\n### Weekly x402 health report\n\nEvery Monday, [`weekly-report.yml`](.github/workflows/weekly-report.yml) turns the last 7 days of the Trust Index into a\nreport: how many Bazaar resources are clean, payable with a caution or not payable, the most common reasons with the fix\nfor each, which networks payable resources accept, and what changed this week (broke, fixed, flaky, new). It names no\nseller or host. The report, its numbers and drafts for X, Discord and Reddit (for a person to review and post) go to\n`reports/<date>.md`, `.json` and `-posts.md` on the `weekly-reports` branch. Locally:\n`node scripts/weekly-report.js --out reports [--index trust-data/index.json]`.\n\nThe same run writes a digest per network, Algorand and the XRP Ledger ([`network-digest.js`](scripts/network-digest.js)):\nhow many endpoints could be paid there, from how many sellers, what changed since last week (new endpoints, ones that\nstopped being payable, new sellers), how many accept only that network, the median price and the biggest sellers. It\nnames payable sellers only; problems are counted. Files: `reports/<date>-<network>.md` and `-posts.md` (X and community\ndrafts), plus `reports/networks/<network>-latest.json`, the snapshot next week compares with. Locally:\n`node scripts/network-digest.js --out reports [--index trust-data/index.json] [--previous reports/networks]`.\n\n## Agent skills\n\nTwo [Agent Skills](https://agentskills.io) in [`skills/`](skills) teach a coding agent (Claude Code, Codex, Cursor and others that read `SKILL.md`) when and how to use Doctor:\n\n- **[`x402-preflight`](skills/x402-preflight/SKILL.md)**: 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.\n- **[`x402-seller-check`](skills/x402-seller-check/SKILL.md)**: 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.\n\n```sh\nnpx skills add Fizzl13/x402-doctor\n```\n\n### Claude Code plugin\n\nIn 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. The Fizzl Agent Wallet and Ichimoku Signal have a plugin each, from [Fizzl13/fizzl-skills](https://github.com/Fizzl13/fizzl-skills), the overview of every Fizzl skill:\n\n```\n/plugin marketplace add Fizzl13/x402-doctor\n/plugin install x402-doctor@fizzl\n/plugin install presign-guard@fizzl\n/plugin install fizzl-agent-wallet@fizzl\n/plugin install ichimoku-signal@fizzl\n```\n\nThe marketplace is [`.claude-plugin/marketplace.json`](.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.\n\n## MCP server\n\n`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`.\n\n| Tool | Price | Returns |\n|---|---|---|\n| `x402_quick_check` | free, 10 calls/hour | pass/warn/fail, the number of problems and the top three |\n| `x402_diagnose` | $0.01 USDC via x402 | Every check with a fix hint, as `GET /api/v1/diagnose` |\n| `x402_preflight` | $0.001 USDC via x402 | go/caution/no_go before paying an endpoint, as `GET /api/v1/preflight` |\n| `x402_preflight_batch` | $0.005 USDC via x402 | The preflight for up to 10 endpoints (`urls`), as `GET /api/v1/preflight/batch` |\n| `x402_preflight_deep` | $0.01 USDC via x402 | The preflight plus diagnosis, history, seller and domain checks, as `GET /api/v1/preflight/deep` |\n| `x402_fix` | $0.05 USDC via x402 | The code changes for your stack, as `GET /api/v1/fix` |\n| `x402_delivery_check` | $0.25 USDC via x402 | One real purchase of the endpoint: a job id now, the result shortly after, as `GET /api/v1/delivery-check` |\n| `x402_delivery_status` | free | The status and, once done, the result of a delivery check, as `GET /api/v1/delivery-check/{id}` |\n| `verify_receipt` | free | Whether a paid answer's signed receipt is valid and for which request, as `POST /api/v1/verify` (see Signed verdicts) |\n| `feedback` | free | Report a bug or a missing feature, as `POST /feedback` (below) |\n\nThe 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.\n\n## Feedback\n\nFound a bug, or missing something? Send it with `POST /feedback` (free, no payment) or the MCP tool `feedback`:\n\n```bash\ncurl -X POST https://x402-doctor.fizzl.eu/feedback \\\n  -H 'content-type: application/json' \\\n  -d '{\"type\": \"feature\", \"message\": \"Also check the facilitator /supported list\", \"endpoint\": \"/api/v1/diagnose\"}'\n```\n\n`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.\n\n## CLI\n\n```bash\nnpx github:Fizzl13/x402-doctor https://api.example.com/paid\nnpx github:Fizzl13/x402-doctor https://api.example.com/paid --method POST --json\n```\n\nExit code `0` on pass/warn, `1` when a check fails (or on warnings with `--strict`), `2` on usage errors.\nThe CLI can diagnose `localhost`, so you can run it against a dev server.\n\n## GitHub Action\n\nCheck your x402 endpoints on every push. The step fails on a broken 402, each\nproblem shows up as an annotation on the run and the PR, and the job summary\nlists what is wrong with the fix next to it. On the\n[GitHub Marketplace](https://github.com/marketplace/actions/x402-doctor-check)\nas **x402 Doctor check**; a public endpoint that passes also gets a badge snippet.\n\n```yaml\n- uses: Fizzl13/x402-doctor-action@v1\n  with:\n    urls: |\n      https://your-api.example.com/paid\n      https://your-api.example.com/other\n```\n\n| Input | Default | |\n| --- | --- | --- |\n| `urls` | (required) | One per line or comma separated. `localhost` works for a dev server started earlier in the job. |\n| `method` | tries GET, then POST | `GET` or `POST` |\n| `fail-on` | `fail` | `fail`: a failed check fails the step. `warn`: warnings too. `never`: report only. |\n\nOutputs: `overall` (`pass`, `warn` or `fail`, the worst across the URLs) and\n`report` (the full reports as JSON). The Action uses the runner's own Node\n(20.18 or newer, as on GitHub-hosted runners) and does not change the job's\nNode version. Read-only: no wallet, no payment.\n\nOr the CLI in a plain `run:` step:\n\n```yaml\n- run: npx -y github:Fizzl13/x402-doctor https://your-api.example.com/paid\n```\n\n## Configuration\n\n| Variable | Purpose |\n|----------|---------|\n| `PORT` | Web server port (default `3001`) |\n| `SOLANA_RPC_URL` | RPC for the Solana payout-account check (default: public mainnet RPC, which rate-limits) |\n| `AGENT_PAYOUT_WALLET` | Base address that receives paid-API payments (or `DOCTOR_PAYOUT_WALLET`) |\n| `AGENT_PAYOUT_WALLET_SOLANA` | Solana address that receives paid-API payments (or `DOCTOR_PAYOUT_WALLET_SOLANA`); needs a USDC token account |\n| `ALGORAND_PAY_TO` | Algorand address that receives paid-API payments in USDC (ASA 31566704), settled by GoPlausible's facilitator (`ALGORAND_FACILITATOR_URL` overrides). The account must have opted in to USDC. Unset or `off`: no Algorand option |\n| `STELLAR_FACILITATOR_KEY` | API key for OpenZeppelin's \"Built on Stellar\" facilitator (generate one at channels.openzeppelin.com/gen); turns on the Stellar option: paid-API payments in USDC on Stellar mainnet, fees paid by the facilitator. `STELLAR_FACILITATOR_URL` points at another facilitator (with or without the key) |\n| `STELLAR_PAY_TO` | Stellar account that receives those payments; default Frits's account (it trusts USDC). The account must have a USDC trustline. `off`: no Stellar option |\n| `BUILDER_CODE` | Base Builder Code (ERC-8021, minted at base.dev) declared in every paid route's 402 as the `builder-code` extension: the buyer's x402 client echoes it and the facilitator writes it into the settlement, so Base attributes the payment. Unset or not a valid code: none |\n| `DOCTOR_PRICE` | Price per paid diagnosis (default `$0.01`) |\n| `DOCTOR_PREFLIGHT_PRICE` | Price per pre-payment check (default `$0.001`) |\n| `DOCTOR_BATCH_PRICE` | Price per batch pre-payment check, up to 10 endpoints (default `$0.005`) |\n| `DOCTOR_DEEP_PRICE` | Price per deep pre-payment check (default `$0.01`) |\n| `DOCTOR_FIX_PRICE` | Price per fix (default `$0.05`) |\n| `FACILITATOR_URL` | x402 facilitator (default PayAI, `https://facilitator.payai.network`) |\n| `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET` | Use Coinbase's CDP facilitator for Base (PayAI stays the fallback). Payments settled through CDP get the route listed in the CDP Bazaar |\n| `CDP_VALIDATE` | `off` stops the `cdp-validate` check, which sends the checked URL to Coinbase's public validate API (`api.cdp.coinbase.com/platform/v2/x402/validate`, no key) |\n| `DOCTOR_SOLANA_FACILITATOR` | With CDP keys set, Solana is settled by PayAI (CDP takes over if PayAI is down). Set to `cdp` to settle Solana through CDP too |\n| `MPP_SECRET` | Turns on MPP payment (method `evm`, USDC on Base, via the same facilitator) next to x402 on the paid routes, for agents that speak MPP (mppx). Any long random string; it signs the challenge ids. Without it only x402 is offered |\n| `OUTREACH_URL` / `OUTREACH_KEY` | Optional, both needed: when the free web check finds a broken endpoint (a fail) whose `/openapi.json` publishes `info.contact.email`, Doctor hands a draft e-mail to the Fizzl wallet server at `OUTREACH_URL` (`https://wallet.fizzl.eu`), authenticated with `OUTREACH_KEY` (the same long random string as there). The wallet server only makes a draft; the owner decides on Telegram or the dashboard whether it is sent. Fizzl's own hosts are skipped, and each host at most once a day |\n| `TYPESAFE_API_KEY` | Optional: TypeSafe's Jev judges a broken endpoint before an outreach draft is made (lib/jev-triage.js): no draft for a test, preview or tunnel service, or for findings that would not hurt a paying agent. Off or failing: drafts as before. Plainly test addresses (staging., sandbox., testnet, tunnels, Vercel previews) and clear real problems (http resource.url on https, an internal or raw cloud-function resource.url, a price ≥ 1.5x the advertised one, no payable option) are decided by code without asking. With `ANTHROPIC_API_KEY` too, Claude decides the cases Jev is unsure about. The same keys as repository secrets let `.github/workflows/triage.yml` rank all Trust Index candidates by hand |\n| `MPP_TEMPO_RECIPIENT` | With `MPP_SECRET`: also accepts MPP method `tempo` (push mode: the agent sends USDC.e on Tempo with the MPP memo and answers with the transaction hash, which the Doctor checks on a Tempo RPC). The address that receives it on Tempo; make sure you hold its key in a wallet that supports Tempo. Without it only `evm` is offered |\n| `MPP_TEMPO_CHAIN` | `4217` (Tempo, default) or `42431` (Moderato testnet, pays in pathUSD) |\n| `MPP_TEMPO_RPC` | Overrides the Tempo RPC (default `https://rpc.tempo.xyz`, or the Moderato one) |\n\n| `USAGE_LOG_TOKEN` | Fine-grained GitHub token with Contents read/write on the usage-log repo only. Every call is logged there (see below) |\n| `USAGE_LOG_REPO` | The private usage-log repo (default `Fizzl13/usage-log`) |\n| `ADMIN_PASSWORD` | Password for `/admin/usage` (any user name). Without it the admin pages do not exist |\n\nWithout either payout wallet the paid route answers 503 and the rest of the app works as before.\n\n### Usage dashboard\n\n`/admin/usage` shows every call to the Fizzl services (x402 Doctor, PlainText, Ichimoku Signal, presign-guard):\ntotals, revenue, paying wallets, calls per day per service, the most asked inputs and the latest calls with what\nwas filled in, the result and the payment (with a Basescan/Solscan link). Each service appends one JSON line per\ncall to `events/<service>/<YYYY-MM-DD>.jsonl` in the private usage-log repo (`lib/usage-log.js`, copied into\neach service); the dashboard reads it back (`lib/usage-reader.js`). 402 challenges, health checks and static\nfiles are not counted, and inputs are cut to 300 characters. Logging never delays or fails a request.\n\n## Payment security (Five Attacks on x402)\n\n[Five Attacks on x402](https://arxiv.org/abs/2605.11781) (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:\n\n| Attack | What goes wrong | Here |\n|---|---|---|\n| I-A Revert-grant | The answer is released before the payment is final, and the payment then fails | The answer is held until the settlement is confirmed: `@x402/express` buffers it, and the XRPL and Solana paths wait for a validated, successful transaction. A failed settlement answers 402, never the content. |\n| I-B Settlement preemption | Someone who sees the payment header submits the authorization first, so the server's own settlement fails | A protocol-level issue; the paper's fix is settlement bound to the facilitator. Here the payment header only travels over HTTPS and is never logged. |\n| II Replay | One payment buys several answers | Each payment is claimed once: EIP-3009 nonces on Base, a settlement cache keyed by the transaction on XRPL and Solana (an XRPL payment through t54 is also refused once its transaction is on the ledger, so a restart does not reopen it), and an MPP Tempo transfer must carry the memo of one challenge (valid for minutes) and is claimed by its hash. An XRPL payment's `InvoiceID` names this service, and credits are taken atomically. |\n| III Cache and header confusion | A shared cache (CDN, proxy) stores a paid answer and serves it to someone who did not pay | 402 challenges are `Cache-Control: no-store`; paid answers carry `private` (added by `@x402/express`) or `no-store`. |\n| IV Server selection | Fake or manipulated listings steer agents to the wrong seller | `openapi.json` carries an ownership proof signed by the payout wallet (x402scan), and the pre-payment check flags impersonation, bait, template farms and payout addresses on the OFAC list. |\n\nDoctor 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.\n\n## Safety\n\nThe web app fetches user-submitted URLs server-side:\n\n- Every connection is checked against private and reserved ranges (RFC 1918, loopback, link-local and cloud\n  metadata, CGNAT, IPv6 ULA/link-local, IPv4-mapped IPv6 in any notation) **at connect time**, so DNS rebinding\n  cannot slip past a separate lookup. Redirects are followed manually and each hop is re-checked.\n- Responses are capped at 2 MB and every request times out after 8 s.\n- `/api/diagnose` (free) is rate-limited per IP; `/api/v1/diagnose` is paid per call and uses the same guards.\n- Everything taken from the diagnosed endpoint is rendered as text, never as HTML.\n\nIt never pays the endpoint it diagnoses: no wallet, no signatures, zero financial risk. A paid probe (sign and settle one minimal payment\nto trace verify/settle failures) is deliberately out of scope here.\n\n## Project structure\n\n```\nserver.js              Express app: /api/diagnose (free), paid API, /openapi.json, /.well-known/x402, static frontend\nlib/paid-api.js        GET /api/v1/diagnose, /preflight (+ /batch, /deep) and /fix behind x402 (Base + Solana USDC)\nlib/recipes.js         The fix per failed check, as code for the detected stack\nlib/stack.js           Which stack runs an endpoint, from its response headers\nlib/preflight.js       Pre-payment check: verdict, recommended option, reasons (cached)\nlib/preflight-plus.js  Batch (up to 10 endpoints) and deep pre-payment checks\nlib/bazaar-index.js    Cached CDP Bazaar index (listing signal for preflight)\nlib/trust-scan.js      Trust Index scan: catalog, polite fetch, 30-day history\nlib/trust-index.js     Reads the published index for preflight and /api/trust\nscripts/trust-scan.js  Daily scan entry point (trust-scan.yml)\npublic/trust.html      /trust page\nlib/media.js           Serves the explainer and paid-fix videos from their branches (/media/explainer.mp4, /media/fix.mp4)\nmedia/explainer/       Explainer video pipeline (script, voice, recording, encoding)\nlib/diagnose.js        The checks\nlib/site-scan.js       MetaMask's site scanner verdict for the domain\nlib/mcp-check.js       x402 over MCP: tools/list, unpaid tools/call, the payment requirement in the tool result\nlib/networks.js        Known networks, USDC (or BNB Chain's dollar tokens) per network, address validation\nlib/safe-fetch.js      SSRF-safe fetch (connect-time IP check, redirects, size cap, timeout)\nbin/x402-doctor.js     CLI\npublic/index.html      Frontend\nskills/                Agent Skills: x402-preflight (buyers) and x402-seller-check (sellers)\n.claude-plugin/        Claude Code plugin marketplace: the skills plus the MCP servers of Doctor and presign-guard\ntest/                  node:test suites (fixture x402 servers, SSRF, CLI, API)\n```\n\n## Tests\n\n```bash\nnpm test\n```\n\nThe suites run the doctor against local fixture servers: a healthy v2 service (Base + Solana, Bazaar, OpenAPI,\nmainnet paywall) and a broken one that reproduces the real bugs above, plus SSRF, size-cap, API and CLI tests.\n\n## License\n\nCopyright (C) 2026 Frits Zwager. Licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only); see [LICENSE](LICENSE). If you run a modified version as a network service, you must offer its source code to the users of that service.\n"}