{
  "markdown": "# x402-micro-tollgate\n\n**Visa for AI agents that can’t hold a card.**\n\nSecond-scale settlement bridge for agent traffic — the Web3 tollbooth that clears USDC micropayments and takes **0.1%**.\n\n> 为千万级无支付能力的 AI Agent 提供秒级过桥清算，抽成 0.1% 的 Web3 版 Visa 收费站\n\n**Security backed by Coinbase CDP.** We don't touch your keys or settle payments on custom cryptography — EIP-3009 authorization nonces are single-use at the CDP facilitator (source of truth for on-chain uniqueness). The gateway hardens four buyer/seller trust surfaces: **payment-proof replay/race**, **SSRF on URL fetch**, **upstream bypass (shared-secret trust header)**, and **Base congestion / settle-latency** (pending + retry same proof — never treat HTTP timeout alone as “payment failed”).\n\n## What it is\n\nA thin self-hosted tollgate in front of your API or MCP tools. When an agent calls a paid route, it gets **HTTP 402 as a price tag** (not a hard deny). It pays a small USDC (or configured USDT) amount; you deliver. Default clearing is **Base + USDC** via **Coinbase CDP**; multi-network accepts are env-driven (see matrix below). Humans can still hit a free path; agents convert at the booth.\n\nSelf-host is free ([MIT](./LICENSE)). Repo: [github.com/kevin2003050666-coder/x402-micro-tollgate](https://github.com/kevin2003050666-coder/x402-micro-tollgate) · Questions: [`2767111713@qq.com`](mailto:2767111713@qq.com?subject=x402-micro-tollgate)\n\n> Not an official Coinbase product. Not a full agent marketplace. Not a billing SaaS. A sharp Visa-style tollbooth. Not fiat custody.\n\n> Why this exists: [MANIFESTO.md](./MANIFESTO.md) — pain, engineering, on-chain proof, MIT.\n## Network × asset matrix\n\nDefault deploy profile: **Base USDC only** (dev: Base Sepolia). Enable multi with `NETWORKS` + `ASSETS` or `ACCEPTS_JSON` / `X402_*` aliases. Browser HTML 402 shows a **chain + asset picker** when more than one accept is configured.\n\n| Network | CAIP-2 | USDC | USDT | Status | Facilitator / notes |\n|---|---|---|---|---|---|\n| Base | `eip155:8453` | native Circle | bridged | **live** | CDP `exact`. FeeSplitterFactory **live** ([deployments/base.json](./contracts/deployments/base.json)) |\n| Base Sepolia | `eip155:84532` | test USDC | — | **live** | CDP testnet |\n| Optimism | `eip155:10` | native | bridged | config-ready | Addresses wired; **not** on current CDP matrix |\n| Arbitrum One | `eip155:42161` | native | bridged | **live** | CDP `exact`; factory stub |\n| Polygon PoS | `eip155:137` | native (not USDC.e) | PoS USDT | **live** | CDP `exact`; factory stub |\n| BNB Smart Chain | `eip155:56` | peg 18 dec | peg 18 dec | config-ready | Not on CDP list |\n| Ethereum | `eip155:1` | native | Tether | config-ready | Facilitator-dependent |\n| Avalanche C-Chain | `eip155:43114` | native | USDT | config-ready | In `@x402/evm` defaults |\n| Celo / Sei | `eip155:42220` / `1329` | USDC | — | config-ready | Catalog extras |\n| Solana | `solana:5eykt…` | SPL USDC | SPL USDT | **experimental** | CDP lists Solana; gateway stubs + optional SVM paywall (`SOLANA_PAY_TO`) |\n| TRON | `tron:mainnet` | — | TRC-20 | **planned** | No scheme in deps — **never** in `accepts[]` |\n\nUSDT on EVM typically uses `extra.assetTransferMethod: \"permit2\"` (not EIP-3009). FeeSplitter production path remains **Base + USDC**; other chains are config stubs under [`contracts/deployments/`](./contracts/deployments/).\n\n### Browser wallets\n\n| Wallet | Role |\n|---|---|\n| Coinbase Smart Wallet (Passkey) | Primary CTA |\n| MetaMask | Via wagmi injected target |\n| Injected | Other browser wallets |\n| WalletConnect | When `WALLETCONNECT_PROJECT_ID` set |\n| Solana paywall | Experimental, behind Solana accepts / `PAYWALL_SVM` |\n| TronLink | **Not offered** (TRON planned only) |\n\n---\n\n## Who it’s for\n\n- **API sellers** who want agents to pay per call — without building a billing console or selling monthly seats\n- **Tool builders** shipping paid MCP endpoints into Cursor / Claude\n- **Operators** who want Visa-like take-rate economics (**0.1%**), not hosting invoices\n\n## How money works\n\n1. **Price tag** — unpaid traffic gets HTTP 402 with the amount due (conversion, not rejection)\n2. **Pay** — the agent settles a USDC micropayment in seconds via Coinbase CDP\n3. **Unlock** — the tollgate proxies to your upstream; you keep ~**99.9%**, protocol take **0.1%**\n\nNo protocol monthly fee. Monetization is the toll — like interchange on card rails.\n\n---\n\n## 1-minute quickstart\n\nRequires Node.js 22+.\n\n```bash\ngit clone https://github.com/kevin2003050666-coder/x402-micro-tollgate\ncd x402-micro-tollgate && cp .env.example .env\n# set CDP_API_KEY_ID, CDP_API_KEY_SECRET, X402_PAY_TO (optional: PUBLIC_BASE_URL, UPSTREAM_URL)\nnpm i && npm start\n# curl http://127.0.0.1:8402/health   → 200\n# curl http://127.0.0.1:8402/x402/discover → 200 (free agent yellow pages)\n# curl http://127.0.0.1:8402/v1/quote → 402\n# curl \"http://127.0.0.1:8402/v1/fetch-md?url=https://example.com\" → 402 (paid HTML→Markdown demo)\n```\n\n**npx one-liners** (no clone):\n\n```bash\nnpx x402-micro-tollgate@0.3.3\nnpx x402-micro-tollgate@0.3.3 --seller 0xYourReceivingAddress --stdio\n```\n\n`--seller` / `-s` sets `X402_PAY_TO` before boot (env vars still work). Default without `--stdio` is HTTP + `/mcp` on port 8402.\n\nAgent / crawler docs: [`llms.txt`](./llms.txt) (also `GET /llms.txt` and `GET /.well-known/llms.txt`) · OpenAPI 3.1: [`docs/openapi.yaml`](./docs/openapi.yaml) · well-known: `GET /.well-known/x402.json`, `GET /.well-known/agent-card.json`\n\nAlternative one-liner: `docker compose up --build`\n\nWithout CDP keys the process runs in **demo mode** (protocol-shaped 402 / MCP PaymentRequired, no on-chain settle).\n\nCursor `mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"x402-micro-tollgate\": {\n      \"url\": \"http://127.0.0.1:8402/mcp\"\n    }\n  }\n}\n```\n\nStdio:\n\n```json\n{\n  \"mcpServers\": {\n    \"x402-micro-tollgate\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"x402-micro-tollgate@0.3.3\",\n        \"--seller\",\n        \"0xYourReceivingAddress\",\n        \"--stdio\"\n      ],\n      \"env\": {\n        \"CDP_API_KEY_ID\": \"...\",\n        \"CDP_API_KEY_SECRET\": \"...\",\n        \"PUBLIC_BASE_URL\": \"https://your.public.host\"\n      }\n    }\n  }\n}\n```\n\n(`X402_PAY_TO` in `env` still works if you omit `--seller`.)\n\n### Library drop-in (permissionless seller)\n\n```ts\nimport express from \"express\";\nimport { x402Tollgate } from \"x402-micro-tollgate\";\n\nconst app = express();\napp.use(\"/v1\", await x402Tollgate({ seller: process.env.SELLER! }));\n```\n\nOr set `SELLER` / `X402_SELLER` in `.env` and run `npm start`. See [Environment](#environment) for the $10 threshold, factory address, and fee-release details.\n\n### Buyer / Agent client\n\nThin TypeScript helper that wraps official `@x402/fetch` + `@x402/evm` `ExactEvmScheme`. On HTTP 402 it parses `PAYMENT-REQUIRED`, checks budgets, signs EIP-3009 once, and retries **once** with `PAYMENT-SIGNATURE` (never loops).\n\n```bash\nnpm i x402-micro-tollgate@0.3.3\n```\n\n```ts\nimport { createX402Fetch } from \"x402-micro-tollgate/client\";\nconst fetch402 = createX402Fetch({ privateKey: process.env.BUYER_KEY as `0x${string}` });\nconst res = await fetch402(\"https://x402-micro-tollgate.onrender.com/v1/quote\");\n```\n\nDefaults: `maxSingleSpendUsdc = 0.05`, `maxTotalSpendUsdc = 1.00` (clear `Error` if exceeded). Circuit breaker (before sign): max **10** paid 402s / **0.05 USDC** per rolling 60s, plus fingerprint dead-loop halt (`maxPaidRequestsPerMinute`, `maxSpendUsdcPerMinute`, `enableFingerprintBreaker`). **Hot-wallet warning:** keep only ~**$5–$10 USDC** on the signing key — treat it as an agent spend faucet, not a treasury.\n\n### Human-delegated agent credit (CLI)\n\nThin PoC: **Human sets `B_max`** → **Agent** loops `createX402Fetch` against a paid URL (auto 402 → pay once → retry). Chrome / Telegram / Session Key / Passkey UI are deferred.\n\n```bash\n# SAFETY: fund BUYER_PRIVATE_KEY with ≤ $5–$10 USDC only. Never commit keys.\nexport BUYER_PRIVATE_KEY=0xYourHotWalletKey\n# optional: TARGET_URL MAX_SINGLE_USDC MAX_TOTAL_USDC ROUNDS\nnpm run poc:buyer-agent\n# or: npx tsx scripts/buyer-agent-poc.ts\n```\n\n| Env | Default | Notes |\n|---|---|---|\n| `BUYER_PRIVATE_KEY` | _(required)_ | `0x…` EOA used to sign EIP-3009 |\n| `TARGET_URL` | `https://x402-micro-tollgate.onrender.com/v1/quote` | Paid route to hit |\n| `MAX_SINGLE_USDC` | `0.05` | Per-call cap (`maxSingleSpendUsdc`) |\n| `MAX_TOTAL_USDC` | `1.00` | Session cap / human `B_max` |\n| `ROUNDS` | `3` | Stop after N calls or when budget exhausted |\n\nThe script imports the **local** client (`../src/client`) so repo CI does not need a live chain pay. For a live demo against the published package, swap the import to `x402-micro-tollgate/client` (same API as `0.3.2+`). Each round prints `status`, `autoPaid`, and `sessionSpendUsdc` on stdout; the safety banner goes to stderr.\n\n---\n\n## Deploy\n\n[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/kevin2003050666-coder/x402-micro-tollgate)\n\nUses [`render.yaml`](./render.yaml): Node 22, `npm start`, health `/health`, env names from [`.env.example`](./.env.example). Set `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, and `X402_PAY_TO` in the dashboard for live settlement; set `PUBLIC_BASE_URL` to your `https://….onrender.com` origin for Bazaar.\n\nOps bump: **redeploy discover + well-known** (force Render GitHub auto-deploy so production picks up `GET /x402/discover`, `GET /.well-known/x402.json`, `GET /.well-known/agent-card.json`, and MCP `fetch_md`). If auto-deploy does not fire within a few minutes after this lands on `main`, use the Render dashboard → Manual Deploy → Deploy latest commit.\n\n**Self-host production:** Docker — `docker compose up --build` (see [`Dockerfile`](./Dockerfile) / [`docker-compose.yml`](./docker-compose.yml)).\n\n---\n\n## Environment\n\n| Variable | Default | Notes |\n|---|---|---|\n| `PORT` | `8402` | HTTP listen port |\n| `UPSTREAM_URL` | _(unset → mock)_ | Your API origin |\n| `UPSTREAM_SHARED_SECRET` / `X402_UPSTREAM_SECRET` | — | Optional. After settle, tollgate injects `X-Tollgate-Secret` + `X-Tollgate-Paid` (HMAC). Upstream must require them and reject public direct hits (not mTLS) |\n| `X402_PAY_TO` | — | EVM receive address (live mode SDK init) |\n| `SELLER` / `X402_SELLER` | — | Permissionless seller EOA (EIP-55 validated; invalid → startup fail) |\n| `FACTORY_ADDRESS` | — | Operator-set `FeeSplitterFactory` for CREATE2 predict when amount ≥ threshold (Base live address in [`contracts/deployments/base.json`](./contracts/deployments/base.json); do not hardcode secrets) |\n| `FEE_FREE_BELOW_USDC` / `X402_FEE_FREE_BELOW_USDC` | `10000000` | Atomic USDC (6 decimals). **&lt; $10** → payTo=seller; **≥ $10** → FeeSplitter |\n| `CDP_API_KEY_ID` / `CDP_API_KEY_SECRET` | — | CDP facilitator (+ Onramp session tokens) |\n| `CDP_CLIENT_API_KEY` | — | Public CDP client key for browser Smart Wallet paywall (safe in frontend). Alias: `CDP_CLIENT_KEY` |\n| `X402_FACILITATOR_URL` / `CDP_FACILITATOR_URL` | CDP default | Optional alternate facilitator base URL for MCP (`createCdpFacilitatorClient({ baseUrl })`). Single-vendor; no multi-facilitator routing yet |\n| `PRICE` | `$0.001` | Network default USDC/USDT price tag |\n| `NETWORK` / `X402_NETWORK` | `eip155:84532` / `8453` | Primary CAIP-2 (aliases: `base`, `optimism`, …) |\n| `NETWORKS` / `X402_NETWORKS` | _(primary only)_ | Comma/JSON list of networks for multi-accept |\n| `ASSETS` / `X402_ASSETS` | `USDC` | Cross with NETWORKS → accepts (USDC, USDT) |\n| `ACCEPTS_JSON` / `X402_ACCEPTS_JSON` | — | Explicit accepts array (overrides NETWORKS×ASSETS) |\n| `FACTORY_ADDRESSES` | — | JSON map caip2 → FeeSplitterFactory (Base live; others optional) |\n| `SOLANA_PAY_TO` | — | Base58 payTo for experimental Solana accepts |\n| `PAYWALL_SVM` | `false` | Force `@x402/paywall` SVM UI (also auto if Solana in accepts) |\n| `WALLETCONNECT_PROJECT_ID` | — | Enables WalletConnect in browser paywall |\n| `X402_MIN_PRICE_USDC` | — | Optional static minimum accept amount (atomic USDC). Effective price = max(PRICE, this, gas floor) |\n| `X402_DYNAMIC_MIN_ENABLED` | `false` | When `true`, Base base-fee oracle may bump min accept / feeFreeBelow if gas would eat too much of the payment |\n| `X402_GAS_COST_MAX_FRACTION` | `0.5` | Bump when estimated gas USD &gt; this fraction of the payment |\n| `X402_GAS_ORACLE_TTL_MS` | `30000` | baseFee cache TTL (clamped 15–60s) |\n| `X402_GAS_RPC_URL` / `BASE_RPC_URL` | public Base RPC | JSON-RPC for `eth_getBlockByNumber` baseFee |\n| `X402_GAS_USED_ESTIMATE` | `100000` | Rough L2 gas units for settle cost estimate |\n| `X402_ETH_USD` | `4000` | Conservative ETH/USD floor (no live FX required) |\n| `X402_ENVIRONMENT` | `development` | or `production` |\n| `GATED_PREFIX` | `/v1` | HTTP paths that require payment |\n| `PUBLIC_BASE_URL` | `http://127.0.0.1:$PORT` | Public `https://` origin for Bazaar resource URLs |\n| `CONTACT_EMAIL` | `2767111713@qq.com` | Landing contact mailto (not a SaaS CTA) |\n| `FEE_BPS` | `10` | Documented operator fee (0.1%). Live settle still pays 100% to `payTo` until `release()` |\n| `FEE_COLLECTOR` | `0xa922…7e30E` | **Fixed operator** wallet for the 0.1% slice after `FeeSplitter.release()` |\n| `MERCHANTS_JSON` | — | Optional hosted multi-tenant registry (not required when `SELLER` is set) |\n| `MERCHANTS_FILE` | `merchants.json` | File path; falls back to `merchants.example.json` / built-in demo when no seller |\n| `DEFAULT_MERCHANT` | `demo` | Used when `?merchant=` / `x-merchant-id` omitted — **agents SHOULD always send merchant id** |\n| `REQUIRE_MERCHANT` | `false` | When `true`, gated paths reject missing merchant id with `400 {error:\"merchant_required\"}` (no demo fallback) |\n| `PAYMENT_DEDUPE_TTL_MS` | `600000` | In-memory payment-proof idempotency window (10 min). CDP facilitator nonce remains source of truth |\n| `PAYMENT_DEDUPE_MAX_ENTRIES` | `10000` | Max keys in the gateway `PaymentDedupeStore` LRU (no Redis; interface is Redis-ready) |\n| `X402_VERIFY_TIMEOUT_MS` | `15000` | Documented verify-phase budget (local / facilitator verify) |\n| `X402_SETTLE_TIMEOUT_MS` | `180000` | Wait for facilitator/on-chain settle before `202 payment_pending` (default 3 min — Base congestion) |\n| `KEEPER_ENABLED` | `false` | Optional FeeSplitter `release()` keeper — **never on by default** |\n| `KEEPER_DRY_RUN` | _(see notes)_ | `true` logs `keeper_would_release` without sending txs |\n| `KEEPER_PRIVATE_KEY` | — | EVM key for live `release()` only (never commit) |\n| `KEEPER_RPC_URL` | Base public RPC | JSON-RPC endpoint for balance + release |\n| `KEEPER_INTERVAL_MS` | `3600000` | Poll interval (1h) |\n| `KEEPER_MIN_USDC` | `1000000` | Min USDC balance (atomic) before `release()` — default $1 |\n\n### Permissionless seller + $10 threshold (0.3.0)\n\n**0.3.0**: set `SELLER` (or use `x402Tollgate({ seller })`) and `FACTORY_ADDRESS` for ≥ $10 CREATE2 routing. No `MERCHANTS_JSON` required.\n\n| Amount (USDC atomic, 6 decimals) | `accepts[].payTo` |\n|---|---|\n| **&lt; `10_000_000` ($10)** | seller EOA — **0 protocol fee** |\n| **≥ `10_000_000` ($10)** | CREATE2-predicted `FeeSplitter` for that seller |\n\nx402 `exact` + EIP-3009 only **credits** `payTo` — it does not execute `FeeSplitter` / factory code and does **not** split in the same transaction. For ≥ $10: set `FACTORY_ADDRESS` to the operator-deployed factory (Base reference: [`contracts/deployments/base.json`](./contracts/deployments/base.json)), call `getOrCreate(seller)` before the first such settle, then `release()` later (99.9% / 0.1%). Address typos send funds to the wrong place — the gateway checksum-validates `SELLER` at startup. Do **not** hardcode private keys or operator secrets in source.\n\n### Merchant registry (optional hosted multi-tenant)\n\nOperator **`FEE_COLLECTOR`** is fixed: `0xa922F38041B5ee227c96A547F106F1330447e30E`. Each merchant gets their own [`FeeSplitter`](./contracts/README.md) (`seller` = merchant wallet, `feeCollector` = operator, `feeBps` = 10). The registry maps `merchantId` → splitter address (`payTo`) + seller for display. When `SELLER` is set, the registry is optional; `?merchant=` still works if you provide `MERCHANTS_JSON`.\n\n**Register a merchant:**\n\n1. Deploy `FeeSplitter` with `seller` = merchant wallet, `feeCollector` = `0xa922F38041B5ee227c96A547F106F1330447e30E`, `feeBps` = 10, and the chain’s native USDC as `asset`.\n2. Add an entry to `MERCHANTS_JSON` (or `merchants.json` / copy from [`merchants.example.json`](./merchants.example.json)):\n   ```json\n   {\n     \"acme\": {\n       \"seller\": \"0xMerchantWallet…\",\n       \"payTo\": \"0xDeployedFeeSplitter…\",\n       \"label\": \"Acme API\"\n     }\n   }\n   ```\n3. Call gated APIs with `?merchant=acme` or header `x-merchant-id: acme` (case-insensitive). **Agents SHOULD always send merchant id.** If omitted, the gateway falls back to `DEFAULT_MERCHANT` (`demo`) — that means traffic (and USDC) can silently land on the demo FeeSplitter. Set `REQUIRE_MERCHANT=true` to reject missing merchant with `400 { \"error\": \"merchant_required\" }`. Unknown merchant on gated paths → `400` `{ \"error\": \"unknown_merchant\" }`.\n4. Free listing: `GET /merchants` (also `/v1/merchants`).\n5. Agent yellow pages: `GET /x402/discover` (alias `GET /discover`) — stable JSON catalog derived from the same registry / `SELLER` / demo. **JSON config stays supported**; there is no on-chain `Registry.sol` $5 stake in this release.\n\n**Note:** The Base demo splitter has `seller` = `feeCollector` = operator — fine for demo. Real merchants need their own splitter with their wallet as `seller`.\n\nCDP `createX402Server` uses a **single global** `payTo` for SDK init (`X402_PAY_TO` or the default merchant splitter). Per-request merchant routing rewrites `PAYMENT-REQUIRED` `accepts[].payTo` to the resolved FeeSplitter (same pattern as the https `resource.url` rewrite).\n\nIf `X402_PAY_TO` is still an EOA and you only have one merchant, behavior stays simple. Multi-merchant production should point each registry `payTo` at a deployed splitter — x402/`exact` credits that splitter via EIP-3009; call `release()` later (manually or via the optional keeper) to send 99.9% / 0.1%. Same contract on **Base / Arbitrum / Polygon** — see the [multi-chain FeeSplitter matrix](./contracts/README.md#multi-chain-usdc-matrix-production). This is **receive → later `release()`**, not an atomic same-transaction split and not OpenZeppelin `PaymentSplitter`.\n\n### Discovery (live) + liquidity roadmap (planned)\n\n**Ship now — Discovery only.** Free route `GET /x402/discover` (alias `/discover`) returns agent-readable yellow pages from **existing** sources (`MERCHANTS_JSON` / merchants file / `SELLER` / built-in demo) + `PUBLIC_BASE_URL`. No 402. Example shape:\n\n```json\n{\n  \"version\": 1,\n  \"network\": \"eip155:8453\",\n  \"updatedAt\": \"2026-09-03T00:00:00.000Z\",\n  \"source\": \"merchants\",\n  \"services\": [{\n    \"id\": \"demo\",\n    \"label\": \"demo (operator is also seller)\",\n    \"endpoint\": \"https://your-host/v1/quote?merchant=demo\",\n    \"mcp\": \"https://your-host/mcp\",\n    \"capabilities\": [\"quote\", \"proxy\", \"fetch-md\"],\n    \"price\": \"$0.001\",\n    \"asset\": \"USDC\",\n    \"payTo\": \"0x…\",\n    \"seller\": \"0x…\",\n    \"status\": \"demo\"\n  }]\n}\n```\n\n`status` is health-honest: `live` only when CDP facilitator credentials + payTo are configured; otherwise `demo` (or `config` when accepts are non-live). This is **not** an on-chain Agent Registry.\n\n**Planned (documented only — do not treat as shipped):**\n\n| Track | Why not now |\n|---|---|\n| **Flash Liquidity Pool** (0-confirm advance across chains) | Capital-intensive: a solo founder cannot hold multi-chain USDC float. 0-confirm advance = **credit risk**. Need a circuit-breaker when the hot wallet balance is insufficient. If Superchain / AggLayer gets sub-second native interoperability, cross-chain friction \\(S_{cross}\\) → 0 and this track may shrink. |\n| **Reverse Bounty** (pay agents to call) | Sybil drain if the seller subsidizes calls. Require rate-limit / per-agent identity / IP+fingerprint gates **before** any `claimBounty`. The current ~$20 Discord/X bounty stays a **manual operator payout** — not an on-chain claim. |\n\nFull notes: [`docs/ROADMAP-LIQUIDITY.md`](./docs/ROADMAP-LIQUIDITY.md). No `depositBounty` / `claimBounty` on FeeSplitter and no fake “live” cross-chain clearing claims in this release.\n\n> 发现层已上线（`GET /x402/discover`）；闪电流动性池与反向赏金仅为规划，见 roadmap。\n\n### Gas vs micropayment floor (optional)\n\nMicropayments ($0.001–$0.01) **assume Base low fees**. When L2 gas spikes, estimated settle cost can eat most of a tiny payment. Opt in with `X402_DYNAMIC_MIN_ENABLED=true`: a cached Base `baseFee` oracle (TTL 15–60s, no RPC spam) estimates gas USD (`gasUsedEstimate * baseFee * X402_ETH_USD`) and, if that exceeds `X402_GAS_COST_MAX_FRACTION` of the payment (default 50%), bumps the displayed/enforced minimum accept amount and may raise `feeFreeBelow` so FeeSplitter+`release()` isn’t used until larger amounts. Default is **OFF** so demo `$0.001` still works under normal conditions. Operators can also set a static `X402_MIN_PRICE_USDC`. Current effective mins are on `GET /health` → `gasFloor`.\n\n### Security (gateway hardening)\n\nFour defenses buyers and sellers should know about:\n\n1. **Payment signature replay / race** — Idempotency key = SHA-256(`PAYMENT-SIGNATURE`). An in-process mutex + `PaymentDedupeStore` (memory today; Redis-shaped interface) does set-if-absent **before** upstream. Concurrent losers get `409 { \"error\": \"payment_already_used\" }`. Durable mark happens **only after settle success**; verify failure releases the pending reservation so honest retries are not bricked.\n2. **SSRF on `/v1/fetch-md`** — Only `http`/`https`; DNS resolve rejects private/bogon/link-local/metadata; DNS is re-checked before fetch (rebinding defense); redirects are disabled.\n3. **Upstream bypass** — Optional `UPSTREAM_SHARED_SECRET`: after payment, proxy injects `X-Tollgate-Secret` / `X-Tollgate-Paid` / `X-Tollgate-Timestamp`. Your upstream must require them (see snippet below). This is a shared-secret MVP, **not** mTLS.\n4. **Base congestion / settle latency** — `X402_SETTLE_TIMEOUT_MS` (default 3 minutes) is separate from the short verify budget. If settle is still in progress when the waiter expires → `202 { \"error\": \"payment_pending\", \"retry_with_same_proof\": true }`. **Do not** create a new payment and **do not** treat the buyer as failed solely because HTTP timed out — retry the **same** proof; the gateway resumes without double-settling.\n\nCDP facilitator is a **single-vendor** settle dependency by default; see [SECURITY.md](./SECURITY.md) for `X402_FACILITATOR_URL` (alternate facilitator when available — no multi-facilitator routing yet).\n\n### Payment proof idempotency + settle pending\n\nCDP x402 `exact` + EIP-3009 authorizations are **single-use at the facilitator** (nonce). The gateway additionally tracks each proof fingerprint as `pending` → `settled` → `consumed`:\n\n| Store state | Client sees |\n|---|---|\n| `pending` (settle in flight / after settle-wait timeout) | `202 payment_pending` + `retry_with_same_proof: true` |\n| `settled` (paid; upstream not delivered yet) | Skip re-settle; proxy once; mark `consumed` |\n| `consumed` | `409 payment_already_used` |\n\nProcess-local only by default (restart clears). Facilitator remains authoritative for on-chain nonce uniqueness.\n\n### Upstream trust header (Express example)\n\n```ts\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport type { RequestHandler } from \"express\";\n\nconst SECRET = process.env.UPSTREAM_SHARED_SECRET!;\n\nexport const requireTollgate: RequestHandler = (req, res, next) => {\n  const secret = req.header(\"x-tollgate-secret\") ?? \"\";\n  const paid = req.header(\"x-tollgate-paid\") ?? \"\";\n  const ts = req.header(\"x-tollgate-timestamp\") ?? \"\";\n  const expected = createHmac(\"sha256\", SECRET)\n    .update(`${ts}.${req.method.toUpperCase()}.${req.path}`, \"utf8\")\n    .digest(\"hex\");\n  const okSecret =\n    secret.length === SECRET.length &&\n    timingSafeEqual(Buffer.from(secret), Buffer.from(SECRET));\n  const okPaid =\n    paid.length === expected.length &&\n    timingSafeEqual(Buffer.from(paid), Buffer.from(expected));\n  if (!okSecret || !okPaid) {\n    res.status(401).json({ error: \"tollgate_required\" });\n    return;\n  }\n  next();\n};\n\n// app.use(requireTollgate); // reject public direct hits\n```\n\nOr import `verifyUpstreamTrustHeaders` from `x402-micro-tollgate` in your upstream process.\n\n### Optional FeeSplitter release keeper\n\nScaffold in [`src/keeper.ts`](./src/keeper.ts). **Off by default** (`KEEPER_ENABLED` unset/false). When enabled, every `KEEPER_INTERVAL_MS` (default 1h) it checks USDC balances of registry `payTo` FeeSplitter addresses and calls `release()` when balance ≥ `KEEPER_MIN_USDC` (default `$1`).\n\n**Warning:** Gas for `release()` can exceed the **0.1%** operator fee on sub-cent payments — hence the min-balance gate. Do **not** turn this on by default on Render. Prefer `KEEPER_DRY_RUN=true` (logs `keeper_would_release`) before using a real `KEEPER_PRIVATE_KEY`. Never commit keys.\n\n---\n\n## Bazaar discovery (agents find sellers)\n\n[Bazaar](https://github.com/coinbase/x402/blob/main/docs/extensions/bazaar.mdx) is the x402 discovery catalog.\n\n- **HTTP**: `createX402Server` auto-injects bazaar; we override with `discoverable: true`, descriptions, and input/output schemas (`GET /v1/quote` + gated prefix proxies).\n- **MCP**: `x402ResourceServer` does **not** auto-declare Bazaar — we register `bazaarResourceServerExtension` and pass `declareDiscoveryExtension({ toolName, inputSchema, … })` on `get_quote` / `proxy_request` / `fetch_md`. Resource URL is `PUBLIC_BASE_URL/mcp` (a real http(s) URL, not a display name).\n- **Origin well-known**: `GET /.well-known/x402.json` (alias `/.well-known/x402`) mirrors discover + lists live paid HTTP/MCP resources for facilitator-independent crawlers.\n\n**To appear in Bazaar:**\n\n1. Set `PUBLIC_BASE_URL` to a public **https** origin (localhost listings are a no-op for real crawlers).\n2. Run with live CDP credentials + `X402_PAY_TO`.\n3. Complete **one successful settlement** through the CDP facilitator (empty-body probes on gated routes return **402**, not 400).\n\nLaunch tip: share your `/mcp` or `/v1/quote` URL in Discord **#x402** after that first settlement.\n\n---\n\n## Pay in browser (Smart Wallet)\n\nWhen a **browser** hits a gated route (`Accept: text/html` + Mozilla UA), the gateway returns a thin HTML paywall instead of JSON. Agents / MCP / `curl` keep the JSON 402 body.\n\n### What you get\n\n1. **Connect / create a Coinbase Smart Wallet** via the official [`@x402/paywall`](https://www.npmjs.com/package/@x402/paywall) EVM UI (Coinbase Wallet connector — Passkey Smart Wallet, **no MetaMask required**).\n2. **Sign** the x402 `exact` EIP-3009 payment on Base (or Base Sepolia in development) and **retry** with `PAYMENT-SIGNATURE`.\n3. **Get USDC** (optional): when server CDP keys are set, the paywall shows a **Get USDC** button that calls `POST /x402/session-token` and opens [Coinbase Onramp](https://docs.cdp.coinbase.com/onramp/introduction/quickstart). Apple Pay / card appear only if Onramp supports them for your domain — this repo does **not** fake Apple Pay outside Onramp, and does **not** claim a guaranteed “10 second Apple Pay” without Portal production access.\n\n### Setup (buyer UX)\n\n```bash\n# .env — server secrets stay on the host\nCDP_API_KEY_ID=…\nCDP_API_KEY_SECRET=…\nX402_PAY_TO=0x…          # or SELLER=0x…\n\n# Public client key only (browser-safe)\nCDP_CLIENT_API_KEY=…     # from portal.cdp.coinbase.com\n\n# Optional but recommended for live demo / Bazaar\nPUBLIC_BASE_URL=https://your.host\nX402_ENVIRONMENT=development   # Base Sepolia (eip155:84532)\n# X402_ENVIRONMENT=production  # Base mainnet (eip155:8453)\n```\n\n| Piece | Role |\n|---|---|\n| `CDP_CLIENT_API_KEY` | Injected into paywall HTML as `window.x402.cdpClientKey` |\n| `CDP_API_KEY_ID` + `CDP_API_KEY_SECRET` | Facilitator settle **and** `POST /x402/session-token` (Onramp JWT) |\n| `POST /x402/session-token` | Free path; body `{ \"addresses\": [{ \"address\": \"0x…\", \"blockchains\": [\"base\"] }], \"assets\": [\"USDC\"] }` → `{ token }` |\n\n### Honesty / production notes\n\n- **MVP networks:** Base Sepolia (`development`) and Base mainnet (`production` / `NETWORK=eip155:8453`).\n- **Onramp production:** enable Onramp in CDP Portal, allowlist your domain, and complete any Apple Pay domain verification Coinbase requires. Until then, Smart Wallet pay + sign + retry still works; Get USDC may error or omit card rails.\n- **CSP:** the paywall ships inline scripts (same as upstream `@x402/paywall`). Prefer not to set a strict `script-src` without nonces on 402 HTML responses.\n- **Security:** wallet private keys never touch the frontend; only the public client key is embedded. Session tokens are minted server-side.\n\nTry it: open `https://your-host/v1/quote` in Chrome after configuring the keys above.\n\n---\n\n## What it does\n\n```\nAgents / clients\n   ├─ HTTP  /v1/*          → x402 402 JSON (agents) or Smart Wallet HTML paywall (browsers)\n   ├─ GET   /v1/fetch-md   → paid HTML→Markdown demo (same x402 gate)\n   ├─ GET   /x402/discover → free agent yellow pages (alias /discover; from merchants JSON)\n   ├─ GET   /.well-known/x402.json → free Bazaar-friendly origin manifest (alias /.well-known/x402)\n   ├─ GET   /.well-known/agent-card.json → free agent card (alias /.well-known/agent.json)\n   ├─ GET   /llms.txt      → free AI-crawler summary (alias /.well-known/llms.txt)\n   ├─ GET   /openapi.yaml  → free OpenAPI 3.1 (alias /docs/openapi.yaml)\n   ├─ POST  /x402/session-token → free Onramp session token (when CDP server keys set)\n   ├─ GET   /merchants     → free merchant registry (id, label, seller, payTo)\n   ├─ GET   /health        → free (+ paywall config flags)\n   ├─ GET   /              → developer landing (EN / 中文)\n   └─ MCP   /mcp           → server_info (free), get_quote + proxy_request + fetch_md (paid + bazaar)\n```\n\n| Surface | Stack |\n|---|---|\n| HTTP | `createX402Server` + `paymentMiddlewareFromHTTPServer` + `@x402/paywall` (browser) |\n| MCP | `x402ResourceServer` + `createCdpFacilitatorClient` + `createPaymentWrapper` + Bazaar extension |\n\nCLI: `npx x402-micro-tollgate@0.3.3` | `--seller 0x…` / `-s` | `--stdio` | `--port N`\n\nAgent SEO: [`llms.txt`](./llms.txt) · [`docs/openapi.yaml`](./docs/openapi.yaml)\n\n---\n\n## Publishing\n\nPackage identity (must stay in sync):\n\n- `package.json` `mcpName` = `io.github.kevin2003050666-coder/x402-micro-tollgate`\n- `server.json` `name` = same string\n\nFlow (needs tokens on a machine that has them — not this VM):\n\n1. Public GitHub mirror at `kevin2003050666-coder/x402-micro-tollgate`\n2. `npm publish` (or tag `v*` → `.github/workflows/publish-mcp.yml`)\n3. `mcp-publisher publish` (OIDC) → official MCP Registry\n4. PulseMCP auto-ingests from the official registry — **do not** submit to PulseMCP directly\n\nUpdate `server.json` `remotes[0].url` to your real public `/mcp` before publishing remotes.\n\n---\n\n## Tests\n\n```bash\nnpm test\n```\n\nNo live CDP credentials required.\n\n---\n\n## License\n\nMIT · See [CONTRIBUTING.md](./CONTRIBUTING.md) · [SECURITY.md](./SECURITY.md)\n",
  "bytes": 31069,
  "sha": "6a8b1f41da25f5e0837240f4da41361b0b2b6117ada01e8c3286a7d010d9fad4",
  "repo_slug": "kevin2003050666-coder/x402-micro-tollgate",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_kevin2003050666_coder_x402_mic_39dfcd0a/readme"
}