x402 Seller
Pay-per-call MCP tools (crypto/DeFi data, web reading, AI tasks) in USDC over x402 on Base.
Install
Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).
x402-seller-mcpclaude mcp add x402-seller -e 'BUYER_PRIVATE_KEY=<your buyer_private_key>' -e 'X402_ORIGIN=https://x402-seller-0ay3.onrender.com' -- npx -y x402-seller-mcp@0.1.1
{
"mcpServers": {
"x402-seller": {
"command": "npx",
"args": [
"-y",
"x402-seller-mcp@0.1.1"
],
"env": {
"BUYER_PRIVATE_KEY": "<your buyer_private_key>",
"X402_ORIGIN": "https://x402-seller-0ay3.onrender.com"
}
}
}
}
Environment variables
BUYER_PRIVATE_KEYsecret EVM private key (0x...) of a throwaway wallet funded with a small amount of USDC on Base. Leave unset for explain-only mode (tool calls describe the 402 payment challenge instead of paying).X402_ORIGINWhich x402-compatible server to expose as MCP tools.
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
cryptomonnaie — pay-per-call API over x402
An Express server that sells paid API endpoints over the x402 protocol (USDC payments on Base), built to be consumed by AI agents.
A client (human or agent) calls a paid endpoint → the server replies
402 Payment Required with the payment requirements → the client signs a
USDC payment and replays the request with the PAYMENT header → a
facilitator verifies and settles the payment on-chain → the server
serves the response. No blockchain key management server-side: it only
holds the receiving address.
Available endpoints
All /api/* routes are paid (x402 payment required), except /health,
/stats, and /.well-known/x402.json, which are free. Every response is
clean JSON — never a raw 500, always {error: "..."} with the right HTTP
status code on any problem (validation, upstream source down, etc.).
Replace $URL with the server's URL (http://localhost:4021 locally, the
Render URL in production) in the examples below.
Crypto prices & gas (dedicated routes, optimized for agent search)
| Endpoint | Price | Example |
|---|---|---|
GET /api/price/eth-usd | $0.005 | curl "$URL/api/price/eth-usd" |
GET /api/price/btc-usd | $0.005 | curl "$URL/api/price/btc-usd" |
GET /api/price/sol-usd | $0.005 | curl "$URL/api/price/sol-usd" |
GET /api/price/usdc-supply | $0.005 | curl "$URL/api/price/usdc-supply" |
GET /api/gas/base | $0.005 | curl "$URL/api/gas/base" |
GET /api/gas/ethereum | $0.005 | curl "$URL/api/gas/ethereum" |
These are thin, single-purpose wrappers around the same sources as
/api/defi/price and /api/chain/gas below — kept as separate routes (with
narrow, intent-matching descriptions) so an agent searching for e.g. "ETH
price USD" or "gas price Base" finds and calls them directly, instead of
having to first discover the generic parameterized endpoint.
Crypto / DeFi data (source DefiLlama, free and open)
⚠️ License note: DefiLlama's terms of service restrict their free API to personal, non-commercial use and prohibit commercial exploitation of the data without prior written agreement (defillama.com/terms, clauses 7 and 8.10). These endpoints (plus the 6
/api/price/*and/api/gas/*ones above, and the 4/api/defi/yields/*sub-routes below, all of which reuse the same DefiLlama sources) are built on it anyway, on the explicit and informed decision of this service's operator (compliance risk accepted) — to be revisited if DefiLlama raises the issue, or by moving to their paid Pro API (pro-api.llama.fi) if needed.
| Endpoint | Price | Example |
|---|---|---|
GET /api/defi/price | $0.005 | curl "$URL/api/defi/price?coins=ethereum,bitcoin" |
GET /api/defi/tvl | $0.005 | curl "$URL/api/defi/tvl?protocol=aave" |
GET /api/defi/tvl-chain | $0.005 | curl "$URL/api/defi/tvl-chain?chain=base" |
GET /api/defi/protocols | $0.005 | curl "$URL/api/defi/protocols?limit=20" |
GET /api/defi/yields | $0.005 | curl "$URL/api/defi/yields?chain=base&min_tvl=1000000" |
GET /api/defi/yields/top | $0.005 | curl "$URL/api/defi/yields/top?limit=10&min_tvl=10000000" |
GET /api/defi/yields/by-token | $0.005 | curl "$URL/api/defi/yields/by-token?symbol=USDC&limit=10" |
GET /api/defi/yields/by-chain | $0.005 | curl "$URL/api/defi/yields/by-chain?chain=base&limit=10" |
GET /api/defi/yields/pool | $0.005 | curl "$URL/api/defi/yields/pool?pool=<pool id>" |
GET /api/defi/stablecoins | $0.005 | curl "$URL/api/defi/stablecoins?limit=20" |
/api/defi/yields/top, /by-token, /by-chain, and /pool are dedicated, intent-matching
routes alongside the generic /api/defi/yields — for an agent searching "best yield for
USDC" or "best yields on Base" rather than discovering the generic parameterized endpoint
first (same rationale as the dedicated /api/price/* and /api/gas/* routes above).
/pool returns one pool's detail plus its last 30 recorded APY/TVL data points, via
DefiLlama's yields.llama.fi/chart/{pool} (verified live against the current DefiLlama
docs before use — see endpoints/defi-yields-pool.js for a note on a doc/reality
mismatch found in the process: the docs list /chart/{pool}'s base URL as api.llama.fi,
but only yields.llama.fi actually serves it; api.llama.fi/chart/{pool} 404s).
On-chain data (public RPC reads via viem, no third-party API)
| Endpoint | Price | Example |
|---|---|---|
GET /api/chain/gas | $0.005 | curl "$URL/api/chain/gas?chain=base" (or chain=ethereum) |
GET /api/chain/block | $0.005 | curl "$URL/api/chain/block?chain=base" |
Web reading & extraction (fetch, readability, and — for extract — Claude Haiku 4.5)
| Endpoint | Price | Example |
|---|---|---|
POST /api/web/read | $0.005 | curl -X POST "$URL/api/web/read" -H "Content-Type: application/json" -d '{"url":"https://en.wikipedia.org/wiki/HTTP_402"}' |
POST /api/web/extract | $0.02 | curl -X POST "$URL/api/web/extract" -H "Content-Type: application/json" -d '{"url":"...","schema":{"type":"object","properties":{"title":{"type":"string"}}}}' |
POST /api/web/read downloads a page and returns its main content as clean
Markdown (readability extraction — boilerplate/nav/ads stripped), so an
agent never has to parse raw HTML. POST /api/web/extract does the same
fetch, then extracts structured JSON from the page according to a
caller-supplied JSON Schema, via Claude Haiku 4.5 — one call instead of
read-then-extract. Both are guarded against SSRF (see lib/web.js): the
target URL must be public http(s), private/loopback/link-local/reserved IP
ranges are refused (checked both on the initial host and on every redirect
hop), the download is capped at 2 MB within a 10 s budget, and the site's
robots.txt is honored (fails open — i.e. allows the fetch — only when
robots.txt itself is unreachable, the same convention real crawlers use).
Open public data
| Endpoint | Price | Source / license | Example |
|---|---|---|---|
GET /api/fx/rates | $0.005 | Frankfurter (MIT, open ECB data) | curl "$URL/api/fx/rates?base=EUR" |
GET /api/github/repo | $0.005 | GitHub REST API | curl "$URL/api/github/repo?full_name=expressjs/express" |
GET /api/npm/package | $0.005 | registry.npmjs.org + api.npmjs.org | curl "$URL/api/npm/package?name=express" |
GET /api/hn/top | $0.005 | Hacker News Firebase API (MIT) | curl "$URL/api/hn/top?limit=20" |
GET /api/wiki/summary | $0.005 | Wikimedia REST API (CC BY-SA 4.0, attribution included in the response) | curl "$URL/api/wiki/summary?title=Bitcoin&lang=en" |
GET /api/dns/lookup | $0.005 | Direct DNS resolution (Node's dns module) | curl "$URL/api/dns/lookup?domain=example.com" |
GET /api/rdap/domain | $0.005 | rdap.org (open protocol, WHOIS's successor) | curl "$URL/api/rdap/domain?domain=example.com" |
AI tasks (Claude Haiku 4.5, ANTHROPIC_API_KEY required)
| Endpoint | Price | Example |
|---|---|---|
POST /api/ai/summarize | $0.01 | curl -X POST "$URL/api/ai/summarize" -H "Content-Type: application/json" -d '{"text":"...","max_sentences":3}' |
POST /api/ai/classify | $0.01 | curl -X POST "$URL/api/ai/classify" -H "Content-Type: application/json" -d '{"text":"...","labels":["positive","negative","neutral"]}' |
POST /api/ai/translate | $0.01 | curl -X POST "$URL/api/ai/translate" -H "Content-Type: application/json" -d '{"text":"...","target_lang":"French"}' |
POST /api/ai/extract | $0.02 | curl -X POST "$URL/api/ai/extract" -H "Content-Type: application/json" -d '{"text":"...","schema":{"type":"object","properties":{"total":{"type":"number"}}}}' |
Premium reseller (Tavily, Serper — real third-party providers, real margin)
| Endpoint | Price | Example |
|---|---|---|
POST /api/search/web | $0.01 | curl -X POST "$URL/api/search/web" -H "Content-Type: application/json" -d '{"query":"latest developments in the x402 protocol","num_results":5}' |
POST /api/search/serp | $0.005 | curl -X POST "$URL/api/search/serp" -H "Content-Type: application/json" -d '{"query":"best crypto payment protocols 2026","country":"us"}' |
Unlike the rest of this server (free/public sources, or a flat-rate AI call), this family resells a paid upstream provider's API per call — so margin, compliance, and upstream outages are real, ongoing concerns, tracked deliberately rather than assumed away.
A third endpoint, POST /api/web/scrape (Tavily Extract), was built, shipped, then
retired on 2026-09-03. It was removed after a real 6-page comparative test (3
JavaScript-rendered pages, a heavy documentation page, a product page, and an article
behind a cookie-consent banner — all confirmed robots.txt-compliant before testing)
against this server's own free /api/web/read: the in-house extractor matched or beat
Tavily Extract on 5 of the 6 pages, usually because Tavily returned a full page dump
(navigation and boilerplate mixed in) where Readability went straight to the actual
content. Tavily's only reproducible advantage was bypassing a bot-detection block that
refused this server's own honestly-identified crawler outright — real, but too narrow to
justify a dedicated $0.02 endpoint. Full test data: docs/RAPPORT-P1-PREMIUM.md.
Compliance basis (verified before writing any code, not assumed). The brief named Exa, Serper, and Firecrawl as candidates. Both Exa and Firecrawl were rejected: their Terms of Service explicitly forbid reselling API output in a commercial product without prior written consent (Exa ToS §4.2(a)(e)(f): no distributing/publishing/offering-for-sale of anything obtained via the Services, no reselling, no building a competitive product; Firecrawl ToS: "Use the Services for any commercial purposes except as expressly authorized by Firecrawl" plus a separate "sell, distribute... based on the Services" prohibition). Two replacements were researched and picked instead:
- Tavily (
api.tavily.com) — replaces Exa for/api/search/web. Its ToS (tavily.com/terms) contains an explicit carve-out for exactly this architecture: §3.2 bans reselling/sublicensing the Services except "integration of the Services in Customer Applications", and a Customer Application is defined (§1.2) to include serving your own third-party end users — provided (§3.5, Acceptable Use Policy §4) those end users never receive the Tavily API key or call Tavily directly (they only ever talk to this server). That's exactly howendpoints/search-web.jsis built. - Serper (
serper.dev) — used for/api/search/serp. SerpApi was checked as an alternative and rejected (subscription-only, no true prepaid credits, and is currently the defendant in active litigation brought by Google over its scraping methods). Serper's own ToS is silent on resale — neither an explicit permission nor a prohibition. The one clause that matters bans mirroring "the materials on any other server as-is with no-value-added" — soendpoints/search-serp.jsdeliberately restructures Serper's raw JSON (renamed/trimmed fields, 3 separate response sections merged into one shape) rather than passing it through verbatim, to stay clearly on the value-added side of that clause. This is a documented risk decision, not a clean bill of health — revisit if Serper ever adds an explicit resale clause either way.
Margin, at the cheapest prepaid tier of each provider (real numbers, verified against each provider's own current pricing docs, cited — not estimates):
| Endpoint | Sale price | Upstream cost | Margin | Upstream unit |
|---|---|---|---|---|
POST /api/search/web | $0.01 | $0.008 | $0.002 (20%) | Tavily pay-as-you-go, $0.008/credit, 1 credit per basic search (docs.tavily.com/documentation/api-credits) |
POST /api/search/serp | $0.005 | $0.001 | $0.004 (80%) | Serper Starter pack, $50/50,000 credits, 1 credit per query up to 10 results (serper.dev's own pricing page was returning a 404 when last checked — figure corroborated by third-party sources, not the primary source; our own account balance confirms $0.001/credit is consistent with real usage) |
/api/search/web's margin is thinner than the "cost × ~2" target set out in the brief —
Tavily's real floor ($0.008/credit) is higher than assumed, and $0.01 was kept as the sale
price anyway (rather than raising to $0.02) to stay priced like the rest of this server's
cheap data endpoints; the price is one constant to change in endpoints/search-web.js if
thicker margin matters more than that. Every successful premium-reseller call appends its
real upstream cost to logs/couts.jsonl (lib/couts-log.js — same DATA_DIR/gitignore
discipline as paiements.jsonl/sondages.jsonl), so actual margin (sale price is already
known and fixed; only the cost side needs tracking) can be checked against these estimates
over time rather than assumed to hold forever.
⚠️ Operational gotcha found shipping the now-retired /api/web/scrape (2026-09-02):
CDP's mainnet facilitator silently rejects payments for endpoints with a long
description. Its first description (557 chars) failed real mainnet payment 5/5 times —
the facilitator's /verify call returned "'paymentPayload' is invalid: must match one of
[x402V2Pay...", which surfaces to the buyer as a bare, unhelpful 402 (looks identical to
"insufficient funds" or "didn't pay at all" — nothing in the response says "description too
long"). Reproduced locally by running this server with NETWORK=base against the real CDP
facilitator (no deploy needed per iteration) and bisecting: every other endpoint's shorter
description settled fine in the same session (the 334-char /api/search/web included), and
trimming to 301 chars fixed it, confirmed with 3/3 real settled mainnet transactions. Root
cause and exact limit not confirmed (CDP's schema isn't public) — the testnet facilitator
did not reproduce this at 557 chars, so always verify a new/lengthened endpoint
description with a real mainnet payment, not just testnet, before trusting it. This
lesson outlives the endpoint that surfaced it — rule of thumb for any future endpoint:
keep description well under ~350 chars.
Failure handling: lib/tavily.js and lib/serper.js collapse every upstream failure
mode — missing API key, network error, any non-2xx response (including an exhausted
credit balance) — to the same clean 503 {"error":"This endpoint is temporarily
unavailable (...)."}, never a raw 500 and never a leaked provider error message. Both
endpoints cache identical repeated requests for 60s (same convention as the rest of this
server, see lib/cache.js) — a cache hit costs nothing upstream, so real margin on
repeated queries is better than the table above.
All the requests above return a 402 Payment Required first — replay them
with an x402 client (see scripts/buyer-test.js for a full example, or
@x402/fetch on the agent side).
Stack
- Node 20+, ESM, Express — no TypeScript.
- x402 v2 packages (current ecosystem, scoped
@x402/*):@x402/express— Express middleware (paymentMiddleware,x402ResourceServer)@x402/core— HTTP facilitator client (HTTPFacilitatorClient)@x402/evm—exactpayment scheme on EVM (server and client)@x402/fetch— buyer side: afetchwrapper that auto-pays 402s@x402/extensions— the Bazaar extension (discovery metadata for agents)@coinbase/x402— CDP facilitator config (mainnet)viem— key generation / EVM signing, RPC reads (/api/chain/*,/api/gas/*)express-rate-limit— per-IP rate limiting on/api/*routes@anthropic-ai/sdk— Claude Haiku 4.5 for the/api/ai/*and/api/web/extractendpointsjsdom+@mozilla/readability— safe HTML parsing and article extraction (the same engine behind Firefox Reader View) for/api/web/*turndown— HTML-to-Markdown conversion for/api/web/*robots-parser— robots.txt compliance for/api/web/*ipaddr.js— private/reserved IP classification for the/api/web/*SSRF guard
The older
x402-express/x402-fetchpackages (v1, unscoped) are deprecated — don't mix them with@x402/*.
Structure
server.js # starts Express, loads endpoints/, mounts the x402 middleware
config.js # reads .env, validates it, maps base-sepolia/base -> CAIP-2
discovery.js # builds the GET /.well-known/x402.json document
payment-log.js # logs every successful payment to logs/paiements.jsonl
sondage-log.js # logs every 402 response served ("probes") to logs/sondages.jsonl
echecs-log.js # logs settlement/upstream failures to logs/echecs.jsonl (see "Observability")
lib/
http.js # fetchJson/fetchText (10s timeout, User-Agent), safeHandler (never a raw 500, logs UpstreamError)
cache.js # 60s in-memory cache for market/network data
anthropic.js # shared Claude Haiku 4.5 client for /api/ai/* and /api/web/extract
chains.js # resolves ?chain=base|ethereum -> viem client, shared gas-price helper
defi.js # shared DefiLlama helpers for /api/price/*
web.js # SSRF-guarded page fetch + readability-to-Markdown extraction for /api/web/*
stats.js # computes GET /stats from the two jsonl logs
stats-daily.js # computes GET /stats/daily (protected) — revenue, top-10 UA
stats-probes.js # computes GET /stats/probes (protected) — full UA/IP long tail, scanner/cible
stats-echecs.js # computes GET /stats/echecs (protected) — last 100 failures + counters
tavily.js # shared Tavily client for /api/search/web (see "Premium reseller")
serper.js # shared Serper.dev client for /api/search/serp (see "Premium reseller")
couts-log.js # logs our own upstream cost per premium-reseller call to logs/couts.jsonl
endpoints/ # one file = one endpoint, auto-loaded
health.js # GET /health (free)
stats.js # GET /stats (free)
defi-tvl.js # GET /api/defi/tvl (paid, $0.005)
defi-price.js # GET /api/defi/price
defi-tvl-chain.js # GET /api/defi/tvl-chain
defi-protocols.js # GET /api/defi/protocols
defi-yields.js # GET /api/defi/yields
defi-yields-top.js # GET /api/defi/yields/top
defi-yields-by-token.js # GET /api/defi/yields/by-token
defi-yields-by-chain.js # GET /api/defi/yields/by-chain
defi-yields-pool.js # GET /api/defi/yields/pool
defi-stablecoins.js # GET /api/defi/stablecoins
price-eth-usd.js # GET /api/price/eth-usd
price-btc-usd.js # GET /api/price/btc-usd
price-sol-usd.js # GET /api/price/sol-usd
price-usdc-supply.js # GET /api/price/usdc-supply
chain-gas.js # GET /api/chain/gas
chain-block.js # GET /api/chain/block
gas-base.js # GET /api/gas/base
gas-ethereum.js # GET /api/gas/ethereum
web-read.js # POST /api/web/read
web-extract.js # POST /api/web/extract
fx-rates.js # GET /api/fx/rates
github-repo.js # GET /api/github/repo
npm-package.js # GET /api/npm/package
hn-top.js # GET /api/hn/top
wiki-summary.js # GET /api/wiki/summary
dns-lookup.js # GET /api/dns/lookup
rdap-domain.js # GET /api/rdap/domain
ai-summarize.js # POST /api/ai/summarize
ai-extract.js # POST /api/ai/extract
ai-classify.js # POST /api/ai/classify
ai-translate.js # POST /api/ai/translate
search-web.js # POST /api/search/web (paid, $0.01 — premium reseller, Tavily)
search-serp.js # POST /api/search/serp (paid, $0.005 — premium reseller, Serper)
scripts/
generate-buyer-wallet.js # generates BUYER_PRIVATE_KEY (viem) + prints the address
buyer-test.js # buyer client: receives the 402, pays, prints the response (path/method/body configurable)
check-bazaar.js # npm run bazaar — queries the CDP facilitator's Bazaar discovery
seed-bazaar.js # npm run seed [-- --only=...] — pays real endpoints so the Bazaar indexes them
seed-hebdo.js # npm run seed-hebdo — weekly seed of a configurable subset (SEED_PATHS), balance guard + retry (see below)
lib/seed-core.js # shared dynamic-discovery + payment loop behind seed-bazaar.js and seed-hebdo.js
importer-cle-cdp.js # npm run cle — imports the CDP key into .env without ever printing it
render.yaml # Render deployment blueprint (Node web service)
logs/paiements.jsonl # successful-payment log (gitignored, created on the first payment)
logs/sondages.jsonl # 402-response log (gitignored, created on the first probe)
logs/seeds.jsonl # weekly seed run summaries (gitignored, LOCAL only — see below)
logs/couts.jsonl # our own upstream cost per premium-reseller call (gitignored, see "Premium reseller")
logs/echecs.jsonl # settlement/upstream failure log (gitignored, created on the first failure)
.env / .env.example # configuration (.env is never committed)
Adding an endpoint
Create endpoints/my-endpoint.js:
export const path = "/api/my-endpoint";
export const method = "GET"; // optional, defaults to GET
export const price = "$0.01"; // null => free
export const description = "What this endpoint does.";
export async function handler(req, res) {
res.json({ hello: "world" });
}
It is loaded automatically at startup. An optional discovery export (via
declareDiscoveryExtension from @x402/extensions/bazaar) describes the
input parameters and an example output — see endpoints/defi-tvl.js.
Write description and discovery in English, phrased around the search
terms an agent would actually type (e.g. "ETH price USD", "summarize
text") — that's what buyer agents match against in the Bazaar and in
/.well-known/x402.json.
Configuration (.env)
| Variable | Role |
|---|---|
NETWORK | base-sepolia (test, default) or base (production) |
BASE_URL | This server's public URL, announced to agents (Bazaar, .well-known/x402.json). Never localhost in production. Empty locally → auto falls back to http://localhost:PORT |
PAY_TO_ADDRESS | EVM address that receives the USDC |
CDP_API_KEY_ID / CDP_API_KEY_SECRET | CDP keys — required only if NETWORK=base |
BUYER_PRIVATE_KEY | Test buyer wallet's private key — never set server-side in production (see render.yaml) |
ANTHROPIC_API_KEY | Required for /api/ai/* and /api/web/extract (Claude Haiku 4.5) — without it, these endpoints return a clean 500 error explaining the missing key |
GITHUB_TOKEN | Optional — raises the GitHub rate limit (60/h → 5000/h) for /api/github/repo. No scope required (public repo data) |
TAVILY_API_KEY | Required for /api/search/web (see "Premium reseller") — without it, it returns a clean 503, never a 500 |
SERPER_API_KEY | Required for /api/search/serp (see "Premium reseller") — without it, returns a clean 503, never a 500 |
PORT | Server port — provided automatically by Render in production, 4021 locally |
Importing the CDP key (npm run cle)
To go to production without copy-pasting CDP_API_KEY_ID/CDP_API_KEY_SECRET
into .env by hand:
npm run cle
- 1st run: creates
CLE_API_CDP.txtat the repo root (a template with 2 fields to fill in) and opens it in TextEdit. Paste the Key ID (one line) and the Secret (can be a multi-line PEM block), save. - 2nd run (
npm run cleagain): reads the file, writesCDP_API_KEY_ID/CDP_API_KEY_SECRETinto.env(the multi-line secret is stored quoted with literal\ns —dotenvconverts them back to real newlines on load), switchesNETWORK=base, deletesCLE_API_CDP.txt, and adds it to.gitignore. The secret is never printed, only its size (number of lines) is confirmed.
Facilitators:
- base-sepolia → public test facilitator
https://x402.org/facilitator, no key. - base → the CDP facilitator (Coinbase Developer Platform), authenticated with
CDP_API_KEY_ID/CDP_API_KEY_SECRET(create keys at https://portal.cdp.coinbase.com).
Quickstart (testnet)
npm install
npm start # starts the server on port 4021
# In another terminal:
npm run generate-buyer-wallet # generates BUYER_PRIVATE_KEY + prints the address
# Fund the address with test USDC: https://faucet.circle.com (Base Sepolia)
npm run buyer-test # pays $0.005 on /api/defi/tvl and prints the response + tx hash
Test another endpoint (path, method, and body configurable):
ENDPOINT_PATH="/api/defi/price?coins=bitcoin" npm run buyer-test
ENDPOINT_PATH="/api/ai/summarize" METHOD=POST \
BODY='{"text":"Long article...","max_sentences":1}' npm run buyer-test
ENDPOINT_PATH="/api/web/read" METHOD=POST \
BODY='{"url":"https://en.wikipedia.org/wiki/HTTP_402"}' npm run buyer-test
Check manually:
curl http://localhost:4021/health # {"ok":true}
curl http://localhost:4021/stats # usage stats, free
curl -i "http://localhost:4021/api/defi/tvl?protocol=aave" # 402 Payment Required
Discovery for agents (Bazaar + .well-known/x402.json)
The Bazaar is the official x402 discovery index (docs.x402.org): it
lives on the facilitator side (GET {facilitator}/discovery/resources),
fed by each route's metadata via @x402/extensions/bazaar. This server's
routes declare that metadata (input schema + example output); on mainnet,
behind the CDP facilitator, they can be indexed and discovered by third-party
agents through that endpoint (no key required to read it).
In addition, GET /.well-known/x402.json lists, server-side, every paid
endpoint directly (absolute URL via BASE_URL, method, description, price,
network, payTo, input/output schema). There is no single official schema
for this file: this document follows the envelope from the IETF draft
"Discovering x402 Payment Capability via DNS and a Well-Known URI"
(x402Version, kind: "resource-server", resources[], docs, updated)
and enriches each resource with the same accepts/extensions.bazaar
fields already used in this server's real 402 responses — see
discovery.js for the detail and its sources.
curl https://x402-seller.onrender.com/.well-known/x402.json
To check what the CDP facilitator has indexed from this server (mainnet only):
npm run bazaar
Weekly automated seed (staying indexed in the Bazaar)
The CDP facilitator de-lists a resource from the Bazaar per endpoint after 30 days without a settled payment on that specific URL (verified directly against docs.cdp.coinbase.com/x402/seller/get-discovered, not assumed — a separate, payment-independent health-probe mechanism also down-ranks/removes an endpoint that fails consecutive availability checks). Real agent traffic alone can't be relied on to keep every endpoint fresh.
A dedicated Render Cron Job (x402-seed-hebdo, created via the Render
API, not in render.yaml — a separate resource on purpose, so it can never
touch the web service's deploys) runs node scripts/seed-hebdo.js every
Monday against a configurable subset (SEED_PATHS, at the top of the
file — the only place to edit it), one retry per endpoint on failure, 3s
pause between calls.
Reseeding every endpoint stopped making sense once the catalog grew and
one price changed (2026-09-03): at $0.440/run (33 endpoints, ~$1.90/month)
against ~$0.005 of real third-party revenue since launch, the cost was
disproportionate — and, per the 30-day-per-endpoint rule above, a weekly
run was already 4x more frequent than the minimum needed anyway, so the
real lever is the endpoint list, not the frequency. SEED_PATHS now
seeds only the 5 /api/defi/yields* routes ($0.05 each, since a pricing
test) plus gas/base and defi/price ($0.005 each) — $0.26/run,
~$1.13/month. The endpoints left out of SEED_PATHS will drop out of the
Bazaar catalog after 30 days without a real payment — an accepted risk (see
docs/ for the dated note); they stay fully served and still listed in this
server's own discovery documents (.well-known/x402.json, openapi.json)
regardless, only the CDP facilitator's own catalog is affected. Before
spending anything the script reads the buyer wallet's real USDC balance on
Base mainnet and refuses to run if it's under $0.30 (~1.15x one run's cost,
same margin ratio as the old $0.50/$0.440 guard, rescaled) — recharge the
wallet and it resumes on its own next week, no code change needed.
scripts/lib/seed-core.js holds the shared discovery+payment logic used by
both this script and scripts/seed-bazaar.js (the on-demand/--only
variant) — one EXAMPLES table, never two lists that can drift apart.
seed-hebdo.js deliberately does not import config.js: it only ever
needs BUYER_PRIVATE_KEY (read from .env locally via dotenv, or from a
real Render env var on the cron job) and TARGET_URL — none of the
seller-side fields (PAY_TO_ADDRESS, CDP keys), which stay out of the cron
job's environment entirely.
Each run appends one JSON summary line to logs/seeds.jsonl (gitignored,
local disk only — Render cron jobs have no persistent disk, so that
write silently no-ops there; the real record of a Render run is its own
logs in the Render dashboard). Run it yourself anytime:
npm run seed-hebdo
Rate limiting and logging
- Rate limit: 60 requests/minute per IP on all
/api/*routes (express-rate-limit). Beyond that, a429response with a clear message..well-known,/health, and/statsare not rate-limited. - Payment log: every successfully settled payment writes a JSON line to
logs/paiements.jsonl(date,endpoint,payer,montant,hash— only data that's already public on-chain, never a secret or signed payment payload). Directory gitignored, created on the first payment. - Probe log: every
402 Payment Requiredresponse actually served writes a JSON line tologs/sondages.jsonl(date,endpoint, a truncated IP — last octet/group zeroed, never the exact client address — anduser_agent). Same append-only jsonl discipline as the payment log; seesondage-log.js. GET /stats(free): aggregates both logs into 402-probe and successful-payment counts per endpoint, over the last 24h and 7d. Contains no sensitive data (no IPs, payer addresses, or transaction hashes) — seelib/stats.js.- Failure log: two cases that used to be silently swallowed now write a
JSON line each to
logs/echecs.jsonl(type: "settlement_failed"fromserver.js'sonAfterSettlewhen a verified payment's settle call itself fails — motif/payer/User-Agent, never a key or full signature;type: "upstream_error"fromlib/http.js'ssafeHandlerwhenever a paid endpoint's handler throws anUpstreamError— endpoint, mapped provider, the real upstream HTTP status when one was received, a short message). Since x402 only settles after a successful handler response, anupstream_errornever means a buyer was charged for a failed request — seeechecs-log.js. GET /stats/probes?key=<STATS_KEY>(protected, same key as/stats/dailybelow) : the full, untruncated long tail of who's probing — by User-Agent and by truncated IP, 24h/7d, each User-Agent taggedprofil: "scanner"(≥10 distinct endpoints touched) or"cible"(fewer — a genuine low-volume prospect wouldn't make/stats/daily's top-10 cut) — seelib/stats-probes.js.GET /stats/echecs?key=<STATS_KEY>(protected): the last 100 lines oflogs/echecs.jsonlplus 24h/7d counters bytype— seelib/stats-echecs.js.
Deploying to Render
The provided render.yaml describes a Node web service (free plan):
- On https://dashboard.render.com → New → Blueprint → connect this
GitHub repo. Render reads
render.yamlautomatically. - Fill in the requested environment variables (
sync: falsein the blueprint = entered by hand, never committed):NETWORK,PAY_TO_ADDRESS,CDP_API_KEY_ID,CDP_API_KEY_SECRET,BASE_URL,ANTHROPIC_API_KEY. BASE_URLmust be the service's Render URL (e.g.https://x402-seller.onrender.com) — never localhost.BUYER_PRIVATE_KEYis never set server-side: it's a test buyer key, unrelated to the service that sells endpoints.- Render provides
PORTautomatically; the server already listens onprocess.env.PORTand0.0.0.0(server.js), andhealthCheckPath: /healthis already configured inrender.yaml.
Going to production (Base mainnet)
- Create a secret API key at https://portal.cdp.coinbase.com and fill in
CDP_API_KEY_ID/CDP_API_KEY_SECRETin.env(or vianpm run cle). - Set
NETWORK=basein.env,BASE_URLto the real public domain, then restart. - Payments arrive as real USDC at
PAY_TO_ADDRESS.
Reference docs
- Protocol and quickstarts: https://x402.gitbook.io/x402
- CDP facilitator and Bazaar: https://docs.cdp.coinbase.com/x402
- Bazaar (discovery layer): https://docs.x402.org/extensions/bazaar
- IETF
.well-knowndraft: https://datatracker.ietf.org/doc/html/draft-hawkins-x402-dns-discovery-01 - Render blueprint: https://render.com/docs/blueprint-spec
Frequently asked questions
What is x402 Seller?
Pay-per-call MCP tools (crypto/DeFi data, web reading, AI tasks) in USDC over x402 on Base.
How do I install x402 Seller?
It runs on your machine. In Claude Code: claude mcp add x402-seller -e 'BUYER_PRIVATE_KEY=<your buyer_private_key>' -e 'X402_ORIGIN=https://x402-seller-0ay3.onrender.com' -- npx -y x402-seller-mcp@0.1.1. For other clients, paste the JSON configuration from this page into their MCP settings.
Does x402 Seller need an API key?
Yes. It expects BUYER_PRIVATE_KEY, which you set in its configuration.
Is x402 Seller open source?
Its code is public on GitHub, but the repository does not declare a standard license.
Who maintains x402 Seller?
It is published by entreprisedaney33-rgb. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.