# x402 Seller [Health: Active]

**Category:** 💰 Finance & Fintech  
**Repository:** https://github.com/entreprisedaney33-rgb/x402-seller  
**GitHub Stars:** 0  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/x402-seller

## Description
Pay-per-call MCP tools (crypto/DeFi data, web reading, AI tasks) in USDC over x402 on Base.

## Claude Desktop Quick Installation
Heuristic fallback — verify the package name and runner against the repository README before running it. Uses `npx` (confidence: low):

```json
"mcpServers": {
  "x402-seller": {
    "command": "npx",
    "args": ["-y","x402-seller"]
  }
}
```

## Documentation & README

# 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](https://defillama.com), 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 how `endpoints/search-web.js` is 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" — so `endpoints/search-serp.js` deliberately
  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](https://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` — `exact` payment scheme on EVM (server and client)
  - `@x402/fetch` — buyer side: a `fetch` wrapper 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/extract` endpoints
  - `jsdom` + `@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-fetch` packages (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`:

```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:

```bash
npm run cle
```

1. **1st run**: creates `CLE_API_CDP.txt` at 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.
2. **2nd run** (`npm run cle` again): reads the file, writes
   `CDP_API_KEY_ID`/`CDP_API_KEY_SECRET` into `.env` (the multi-line secret
   is stored quoted with literal `\n`s — `dotenv` converts them back to real
   newlines on load), switches `NETWORK=base`, deletes `CLE_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)

```bash
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):

```bash
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:

```bash
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.

```bash
curl https://x402-seller.onrender.com/.well-known/x402.json
```

To check what the CDP facilitator has indexed from this server (mainnet
only):

```bash
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:

```bash
npm run seed-hebdo
```

## Rate limiting and logging

- **Rate limit**: 60 requests/minute per IP on all `/api/*` routes
  (`express-rate-limit`). Beyond that, a `429` response with a clear
  message. `.well-known`, `/health`, and `/stats` are 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 Required` response actually served
  writes a JSON line to `logs/sondages.jsonl` (`date`, `endpoint`, a
  **truncated** IP — last octet/group zeroed, never the exact client
  address — and `user_agent`). Same append-only jsonl discipline as the
  payment log; see `sondage-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) — see `lib/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"` from
  `server.js`'s `onAfterSettle` when a verified payment's settle call
  itself fails — motif/payer/User-Agent, never a key or full signature;
  `type: "upstream_error"` from `lib/http.js`'s `safeHandler` whenever a
  paid endpoint's handler throws an `UpstreamError` — endpoint, mapped
  provider, the real upstream HTTP status when one was received, a short
  message). Since x402 only settles after a successful handler response, an
  `upstream_error` never means a buyer was charged for a failed request —
  see `echecs-log.js`.
- **`GET /stats/probes?key=<STATS_KEY>`** (protected, same key as
  `/stats/daily` below) : the full, untruncated long tail of who's probing
  — by User-Agent and by truncated IP, 24h/7d, each User-Agent tagged
  `profil: "scanner"` (≥10 distinct endpoints touched) or `"cible"` (fewer
  — a genuine low-volume prospect wouldn't make `/stats/daily`'s top-10
  cut) — see `lib/stats-probes.js`.
- **`GET /stats/echecs?key=<STATS_KEY>`** (protected): the last 100 lines
  of `logs/echecs.jsonl` plus 24h/7d counters by `type` — see
  `lib/stats-echecs.js`.

## Deploying to Render

The provided `render.yaml` describes a Node web service (free plan):

1. On https://dashboard.render.com → **New** → **Blueprint** → connect this
   GitHub repo. Render reads `render.yaml` automatically.
2. Fill in the requested environment variables (`sync: false` in the
   blueprint = entered by hand, never committed): `NETWORK`,
   `PAY_TO_ADDRESS`, `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `BASE_URL`,
   `ANTHROPIC_API_KEY`.
3. `BASE_URL` must be the service's Render URL (e.g.
   `https://x402-seller.onrender.com`) — **never** localhost.
4. `BUYER_PRIVATE_KEY` is **never** set server-side: it's a test buyer key,
   unrelated to the service that sells endpoints.
5. Render provides `PORT` automatically; the server already listens on
   `process.env.PORT` and `0.0.0.0` (`server.js`), and
   `healthCheckPath: /health` is already configured in `render.yaml`.

## Going to production (Base mainnet)

1. Create a secret API key at https://portal.cdp.coinbase.com and fill in
   `CDP_API_KEY_ID` / `CDP_API_KEY_SECRET` in `.env` (or via `npm run cle`).
2. Set `NETWORK=base` in `.env`, `BASE_URL` to the real public domain, then
   restart.
3. 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-known` draft: https://datatracker.ietf.org/doc/html/draft-hawkins-x402-dns-discovery-01
- Render blueprint: https://render.com/docs/blueprint-spec

