# x402 Micro Tollgate [Health: Active]

**Category:** ☁️ Cloud Platforms  
**Repository:** https://github.com/kevin2003050666-coder/x402-micro-tollgate  
**GitHub Stars:** 0  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/x402-micro-tollgate

## Description
x402 HTTP 402 + MCP paywall: charge USDC per agent call. Self-host via npx or Docker.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "x402-micro-tollgate": {
    "command": "npx",
    "args": ["-y","one-liners"]
  }
}
```

## Documentation & README

# x402-micro-tollgate

**Visa for AI agents that can’t hold a card.**

Second-scale settlement bridge for agent traffic — the Web3 tollbooth that clears USDC micropayments and takes **0.1%**.

> 为千万级无支付能力的 AI Agent 提供秒级过桥清算，抽成 0.1% 的 Web3 版 Visa 收费站

**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”).

## What it is

A 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.

Self-host is free ([MIT](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/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)

> Not an official Coinbase product. Not a full agent marketplace. Not a billing SaaS. A sharp Visa-style tollbooth. Not fiat custody.

> Why this exists: [MANIFESTO.md](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/MANIFESTO.md) — pain, engineering, on-chain proof, MIT.
## Network × asset matrix

Default 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.

| Network | CAIP-2 | USDC | USDT | Status | Facilitator / notes |
|---|---|---|---|---|---|
| Base | `eip155:8453` | native Circle | bridged | **live** | CDP `exact`. FeeSplitterFactory **live** ([deployments/base.json](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/contracts/deployments/base.json)) |
| Base Sepolia | `eip155:84532` | test USDC | — | **live** | CDP testnet |
| Optimism | `eip155:10` | native | bridged | config-ready | Addresses wired; **not** on current CDP matrix |
| Arbitrum One | `eip155:42161` | native | bridged | **live** | CDP `exact`; factory stub |
| Polygon PoS | `eip155:137` | native (not USDC.e) | PoS USDT | **live** | CDP `exact`; factory stub |
| BNB Smart Chain | `eip155:56` | peg 18 dec | peg 18 dec | config-ready | Not on CDP list |
| Ethereum | `eip155:1` | native | Tether | config-ready | Facilitator-dependent |
| Avalanche C-Chain | `eip155:43114` | native | USDT | config-ready | In `@x402/evm` defaults |
| Celo / Sei | `eip155:42220` / `1329` | USDC | — | config-ready | Catalog extras |
| Solana | `solana:5eykt…` | SPL USDC | SPL USDT | **experimental** | CDP lists Solana; gateway stubs + optional SVM paywall (`SOLANA_PAY_TO`) |
| TRON | `tron:mainnet` | — | TRC-20 | **planned** | No scheme in deps — **never** in `accepts[]` |

USDT on EVM typically uses `extra.assetTransferMethod: "permit2"` (not EIP-3009). FeeSplitter production path remains **Base + USDC**; other chains are config stubs under [`contracts/deployments/`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/contracts/deployments/).

### Browser wallets

| Wallet | Role |
|---|---|
| Coinbase Smart Wallet (Passkey) | Primary CTA |
| MetaMask | Via wagmi injected target |
| Injected | Other browser wallets |
| WalletConnect | When `WALLETCONNECT_PROJECT_ID` set |
| Solana paywall | Experimental, behind Solana accepts / `PAYWALL_SVM` |
| TronLink | **Not offered** (TRON planned only) |

---

## Who it’s for

- **API sellers** who want agents to pay per call — without building a billing console or selling monthly seats
- **Tool builders** shipping paid MCP endpoints into Cursor / Claude
- **Operators** who want Visa-like take-rate economics (**0.1%**), not hosting invoices

## How money works

1. **Price tag** — unpaid traffic gets HTTP 402 with the amount due (conversion, not rejection)
2. **Pay** — the agent settles a USDC micropayment in seconds via Coinbase CDP
3. **Unlock** — the tollgate proxies to your upstream; you keep ~**99.9%**, protocol take **0.1%**

No protocol monthly fee. Monetization is the toll — like interchange on card rails.

---

## 1-minute quickstart

Requires Node.js 22+.

```bash
git clone https://github.com/kevin2003050666-coder/x402-micro-tollgate
cd x402-micro-tollgate && cp .env.example .env
# set CDP_API_KEY_ID, CDP_API_KEY_SECRET, X402_PAY_TO (optional: PUBLIC_BASE_URL, UPSTREAM_URL)
npm i && npm start
# curl http://127.0.0.1:8402/health   → 200
# curl http://127.0.0.1:8402/x402/discover → 200 (free agent yellow pages)
# curl http://127.0.0.1:8402/v1/quote → 402
# curl "http://127.0.0.1:8402/v1/fetch-md?url=https://example.com" → 402 (paid HTML→Markdown demo)
```

**npx one-liners** (no clone):

```bash
npx x402-micro-tollgate@0.3.3
npx x402-micro-tollgate@0.3.3 --seller 0xYourReceivingAddress --stdio
```

`--seller` / `-s` sets `X402_PAY_TO` before boot (env vars still work). Default without `--stdio` is HTTP + `/mcp` on port 8402.

Agent / crawler docs: [`llms.txt`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/llms.txt) (also `GET /llms.txt` and `GET /.well-known/llms.txt`) · OpenAPI 3.1: [`docs/openapi.yaml`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/docs/openapi.yaml) · well-known: `GET /.well-known/x402.json`, `GET /.well-known/agent-card.json`

Alternative one-liner: `docker compose up --build`

Without CDP keys the process runs in **demo mode** (protocol-shaped 402 / MCP PaymentRequired, no on-chain settle).

Cursor `mcp.json`:

```json
{
  "mcpServers": {
    "x402-micro-tollgate": {
      "url": "http://127.0.0.1:8402/mcp"
    }
  }
}
```

Stdio:

```json
{
  "mcpServers": {
    "x402-micro-tollgate": {
      "command": "npx",
      "args": [
        "x402-micro-tollgate@0.3.3",
        "--seller",
        "0xYourReceivingAddress",
        "--stdio"
      ],
      "env": {
        "CDP_API_KEY_ID": "...",
        "CDP_API_KEY_SECRET": "...",
        "PUBLIC_BASE_URL": "https://your.public.host"
      }
    }
  }
}
```

(`X402_PAY_TO` in `env` still works if you omit `--seller`.)

### Library drop-in (permissionless seller)

```ts
import express from "express";
import { x402Tollgate } from "x402-micro-tollgate";

const app = express();
app.use("/v1", await x402Tollgate({ seller: process.env.SELLER! }));
```

Or set `SELLER` / `X402_SELLER` in `.env` and run `npm start`. See [Environment](#environment) for the $10 threshold, factory address, and fee-release details.

### Buyer / Agent client

Thin 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).

```bash
npm i x402-micro-tollgate@0.3.3
```

```ts
import { createX402Fetch } from "x402-micro-tollgate/client";
const fetch402 = createX402Fetch({ privateKey: process.env.BUYER_KEY as `0x${string}` });
const res = await fetch402("https://x402-micro-tollgate.onrender.com/v1/quote");
```

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

### Human-delegated agent credit (CLI)

Thin 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.

```bash
# SAFETY: fund BUYER_PRIVATE_KEY with ≤ $5–$10 USDC only. Never commit keys.
export BUYER_PRIVATE_KEY=0xYourHotWalletKey
# optional: TARGET_URL MAX_SINGLE_USDC MAX_TOTAL_USDC ROUNDS
npm run poc:buyer-agent
# or: npx tsx scripts/buyer-agent-poc.ts
```

| Env | Default | Notes |
|---|---|---|
| `BUYER_PRIVATE_KEY` | _(required)_ | `0x…` EOA used to sign EIP-3009 |
| `TARGET_URL` | `https://x402-micro-tollgate.onrender.com/v1/quote` | Paid route to hit |
| `MAX_SINGLE_USDC` | `0.05` | Per-call cap (`maxSingleSpendUsdc`) |
| `MAX_TOTAL_USDC` | `1.00` | Session cap / human `B_max` |
| `ROUNDS` | `3` | Stop after N calls or when budget exhausted |

The 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.

---

## Deploy

[![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)

Uses [`render.yaml`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/render.yaml): Node 22, `npm start`, health `/health`, env names from [`.env.example`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/.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.

Ops 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.

**Self-host production:** Docker — `docker compose up --build` (see [`Dockerfile`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/Dockerfile) / [`docker-compose.yml`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/docker-compose.yml)).

---

## Environment

| Variable | Default | Notes |
|---|---|---|
| `PORT` | `8402` | HTTP listen port |
| `UPSTREAM_URL` | _(unset → mock)_ | Your API origin |
| `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) |
| `X402_PAY_TO` | — | EVM receive address (live mode SDK init) |
| `SELLER` / `X402_SELLER` | — | Permissionless seller EOA (EIP-55 validated; invalid → startup fail) |
| `FACTORY_ADDRESS` | — | Operator-set `FeeSplitterFactory` for CREATE2 predict when amount ≥ threshold (Base live address in [`contracts/deployments/base.json`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/contracts/deployments/base.json); do not hardcode secrets) |
| `FEE_FREE_BELOW_USDC` / `X402_FEE_FREE_BELOW_USDC` | `10000000` | Atomic USDC (6 decimals). **&lt; $10** → payTo=seller; **≥ $10** → FeeSplitter |
| `CDP_API_KEY_ID` / `CDP_API_KEY_SECRET` | — | CDP facilitator (+ Onramp session tokens) |
| `CDP_CLIENT_API_KEY` | — | Public CDP client key for browser Smart Wallet paywall (safe in frontend). Alias: `CDP_CLIENT_KEY` |
| `X402_FACILITATOR_URL` / `CDP_FACILITATOR_URL` | CDP default | Optional alternate facilitator base URL for MCP (`createCdpFacilitatorClient({ baseUrl })`). Single-vendor; no multi-facilitator routing yet |
| `PRICE` | `$0.001` | Network default USDC/USDT price tag |
| `NETWORK` / `X402_NETWORK` | `eip155:84532` / `8453` | Primary CAIP-2 (aliases: `base`, `optimism`, …) |
| `NETWORKS` / `X402_NETWORKS` | _(primary only)_ | Comma/JSON list of networks for multi-accept |
| `ASSETS` / `X402_ASSETS` | `USDC` | Cross with NETWORKS → accepts (USDC, USDT) |
| `ACCEPTS_JSON` / `X402_ACCEPTS_JSON` | — | Explicit accepts array (overrides NETWORKS×ASSETS) |
| `FACTORY_ADDRESSES` | — | JSON map caip2 → FeeSplitterFactory (Base live; others optional) |
| `SOLANA_PAY_TO` | — | Base58 payTo for experimental Solana accepts |
| `PAYWALL_SVM` | `false` | Force `@x402/paywall` SVM UI (also auto if Solana in accepts) |
| `WALLETCONNECT_PROJECT_ID` | — | Enables WalletConnect in browser paywall |
| `X402_MIN_PRICE_USDC` | — | Optional static minimum accept amount (atomic USDC). Effective price = max(PRICE, this, gas floor) |
| `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 |
| `X402_GAS_COST_MAX_FRACTION` | `0.5` | Bump when estimated gas USD &gt; this fraction of the payment |
| `X402_GAS_ORACLE_TTL_MS` | `30000` | baseFee cache TTL (clamped 15–60s) |
| `X402_GAS_RPC_URL` / `BASE_RPC_URL` | public Base RPC | JSON-RPC for `eth_getBlockByNumber` baseFee |
| `X402_GAS_USED_ESTIMATE` | `100000` | Rough L2 gas units for settle cost estimate |
| `X402_ETH_USD` | `4000` | Conservative ETH/USD floor (no live FX required) |
| `X402_ENVIRONMENT` | `development` | or `production` |
| `GATED_PREFIX` | `/v1` | HTTP paths that require payment |
| `PUBLIC_BASE_URL` | `http://127.0.0.1:$PORT` | Public `https://` origin for Bazaar resource URLs |
| `CONTACT_EMAIL` | `2767111713@qq.com` | Landing contact mailto (not a SaaS CTA) |
| `FEE_BPS` | `10` | Documented operator fee (0.1%). Live settle still pays 100% to `payTo` until `release()` |
| `FEE_COLLECTOR` | `0xa922…7e30E` | **Fixed operator** wallet for the 0.1% slice after `FeeSplitter.release()` |
| `MERCHANTS_JSON` | — | Optional hosted multi-tenant registry (not required when `SELLER` is set) |
| `MERCHANTS_FILE` | `merchants.json` | File path; falls back to `merchants.example.json` / built-in demo when no seller |
| `DEFAULT_MERCHANT` | `demo` | Used when `?merchant=` / `x-merchant-id` omitted — **agents SHOULD always send merchant id** |
| `REQUIRE_MERCHANT` | `false` | When `true`, gated paths reject missing merchant id with `400 {error:"merchant_required"}` (no demo fallback) |
| `PAYMENT_DEDUPE_TTL_MS` | `600000` | In-memory payment-proof idempotency window (10 min). CDP facilitator nonce remains source of truth |
| `PAYMENT_DEDUPE_MAX_ENTRIES` | `10000` | Max keys in the gateway `PaymentDedupeStore` LRU (no Redis; interface is Redis-ready) |
| `X402_VERIFY_TIMEOUT_MS` | `15000` | Documented verify-phase budget (local / facilitator verify) |
| `X402_SETTLE_TIMEOUT_MS` | `180000` | Wait for facilitator/on-chain settle before `202 payment_pending` (default 3 min — Base congestion) |
| `KEEPER_ENABLED` | `false` | Optional FeeSplitter `release()` keeper — **never on by default** |
| `KEEPER_DRY_RUN` | _(see notes)_ | `true` logs `keeper_would_release` without sending txs |
| `KEEPER_PRIVATE_KEY` | — | EVM key for live `release()` only (never commit) |
| `KEEPER_RPC_URL` | Base public RPC | JSON-RPC endpoint for balance + release |
| `KEEPER_INTERVAL_MS` | `3600000` | Poll interval (1h) |
| `KEEPER_MIN_USDC` | `1000000` | Min USDC balance (atomic) before `release()` — default $1 |

### Permissionless seller + $10 threshold (0.3.0)

**0.3.0**: set `SELLER` (or use `x402Tollgate({ seller })`) and `FACTORY_ADDRESS` for ≥ $10 CREATE2 routing. No `MERCHANTS_JSON` required.

| Amount (USDC atomic, 6 decimals) | `accepts[].payTo` |
|---|---|
| **&lt; `10_000_000` ($10)** | seller EOA — **0 protocol fee** |
| **≥ `10_000_000` ($10)** | CREATE2-predicted `FeeSplitter` for that seller |

x402 `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`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/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.

### Merchant registry (optional hosted multi-tenant)

Operator **`FEE_COLLECTOR`** is fixed: `0xa922F38041B5ee227c96A547F106F1330447e30E`. Each merchant gets their own [`FeeSplitter`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/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`.

**Register a merchant:**

1. Deploy `FeeSplitter` with `seller` = merchant wallet, `feeCollector` = `0xa922F38041B5ee227c96A547F106F1330447e30E`, `feeBps` = 10, and the chain’s native USDC as `asset`.
2. Add an entry to `MERCHANTS_JSON` (or `merchants.json` / copy from [`merchants.example.json`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/merchants.example.json)):
   ```json
   {
     "acme": {
       "seller": "0xMerchantWallet…",
       "payTo": "0xDeployedFeeSplitter…",
       "label": "Acme API"
     }
   }
   ```
3. 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" }`.
4. Free listing: `GET /merchants` (also `/v1/merchants`).
5. 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.

**Note:** The Base demo splitter has `seller` = `feeCollector` = operator — fine for demo. Real merchants need their own splitter with their wallet as `seller`.

CDP `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).

If `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](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/contracts/README.md#multi-chain-usdc-matrix-production). This is **receive → later `release()`**, not an atomic same-transaction split and not OpenZeppelin `PaymentSplitter`.

### Discovery (live) + liquidity roadmap (planned)

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

```json
{
  "version": 1,
  "network": "eip155:8453",
  "updatedAt": "2026-09-03T00:00:00.000Z",
  "source": "merchants",
  "services": [{
    "id": "demo",
    "label": "demo (operator is also seller)",
    "endpoint": "https://your-host/v1/quote?merchant=demo",
    "mcp": "https://your-host/mcp",
    "capabilities": ["quote", "proxy", "fetch-md"],
    "price": "$0.001",
    "asset": "USDC",
    "payTo": "0x…",
    "seller": "0x…",
    "status": "demo"
  }]
}
```

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

**Planned (documented only — do not treat as shipped):**

| Track | Why not now |
|---|---|
| **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. |
| **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. |

Full notes: [`docs/ROADMAP-LIQUIDITY.md`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/docs/ROADMAP-LIQUIDITY.md). No `depositBounty` / `claimBounty` on FeeSplitter and no fake “live” cross-chain clearing claims in this release.

> 发现层已上线（`GET /x402/discover`）；闪电流动性池与反向赏金仅为规划，见 roadmap。

### Gas vs micropayment floor (optional)

Micropayments ($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`.

### Security (gateway hardening)

Four defenses buyers and sellers should know about:

1. **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.
2. **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.
3. **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.
4. **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.

CDP facilitator is a **single-vendor** settle dependency by default; see [SECURITY.md](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/SECURITY.md) for `X402_FACILITATOR_URL` (alternate facilitator when available — no multi-facilitator routing yet).

### Payment proof idempotency + settle pending

CDP x402 `exact` + EIP-3009 authorizations are **single-use at the facilitator** (nonce). The gateway additionally tracks each proof fingerprint as `pending` → `settled` → `consumed`:

| Store state | Client sees |
|---|---|
| `pending` (settle in flight / after settle-wait timeout) | `202 payment_pending` + `retry_with_same_proof: true` |
| `settled` (paid; upstream not delivered yet) | Skip re-settle; proxy once; mark `consumed` |
| `consumed` | `409 payment_already_used` |

Process-local only by default (restart clears). Facilitator remains authoritative for on-chain nonce uniqueness.

### Upstream trust header (Express example)

```ts
import { createHmac, timingSafeEqual } from "node:crypto";
import type { RequestHandler } from "express";

const SECRET = process.env.UPSTREAM_SHARED_SECRET!;

export const requireTollgate: RequestHandler = (req, res, next) => {
  const secret = req.header("x-tollgate-secret") ?? "";
  const paid = req.header("x-tollgate-paid") ?? "";
  const ts = req.header("x-tollgate-timestamp") ?? "";
  const expected = createHmac("sha256", SECRET)
    .update(`${ts}.${req.method.toUpperCase()}.${req.path}`, "utf8")
    .digest("hex");
  const okSecret =
    secret.length === SECRET.length &&
    timingSafeEqual(Buffer.from(secret), Buffer.from(SECRET));
  const okPaid =
    paid.length === expected.length &&
    timingSafeEqual(Buffer.from(paid), Buffer.from(expected));
  if (!okSecret || !okPaid) {
    res.status(401).json({ error: "tollgate_required" });
    return;
  }
  next();
};

// app.use(requireTollgate); // reject public direct hits
```

Or import `verifyUpstreamTrustHeaders` from `x402-micro-tollgate` in your upstream process.

### Optional FeeSplitter release keeper

Scaffold in [`src/keeper.ts`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/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`).

**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.

---

## Bazaar discovery (agents find sellers)

[Bazaar](https://github.com/coinbase/x402/blob/main/docs/extensions/bazaar.mdx) is the x402 discovery catalog.

- **HTTP**: `createX402Server` auto-injects bazaar; we override with `discoverable: true`, descriptions, and input/output schemas (`GET /v1/quote` + gated prefix proxies).
- **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).
- **Origin well-known**: `GET /.well-known/x402.json` (alias `/.well-known/x402`) mirrors discover + lists live paid HTTP/MCP resources for facilitator-independent crawlers.

**To appear in Bazaar:**

1. Set `PUBLIC_BASE_URL` to a public **https** origin (localhost listings are a no-op for real crawlers).
2. Run with live CDP credentials + `X402_PAY_TO`.
3. Complete **one successful settlement** through the CDP facilitator (empty-body probes on gated routes return **402**, not 400).

Launch tip: share your `/mcp` or `/v1/quote` URL in Discord **#x402** after that first settlement.

---

## Pay in browser (Smart Wallet)

When 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.

### What you get

1. **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**).
2. **Sign** the x402 `exact` EIP-3009 payment on Base (or Base Sepolia in development) and **retry** with `PAYMENT-SIGNATURE`.
3. **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.

### Setup (buyer UX)

```bash
# .env — server secrets stay on the host
CDP_API_KEY_ID=…
CDP_API_KEY_SECRET=…
X402_PAY_TO=0x…          # or SELLER=0x…

# Public client key only (browser-safe)
CDP_CLIENT_API_KEY=…     # from portal.cdp.coinbase.com

# Optional but recommended for live demo / Bazaar
PUBLIC_BASE_URL=https://your.host
X402_ENVIRONMENT=development   # Base Sepolia (eip155:84532)
# X402_ENVIRONMENT=production  # Base mainnet (eip155:8453)
```

| Piece | Role |
|---|---|
| `CDP_CLIENT_API_KEY` | Injected into paywall HTML as `window.x402.cdpClientKey` |
| `CDP_API_KEY_ID` + `CDP_API_KEY_SECRET` | Facilitator settle **and** `POST /x402/session-token` (Onramp JWT) |
| `POST /x402/session-token` | Free path; body `{ "addresses": [{ "address": "0x…", "blockchains": ["base"] }], "assets": ["USDC"] }` → `{ token }` |

### Honesty / production notes

- **MVP networks:** Base Sepolia (`development`) and Base mainnet (`production` / `NETWORK=eip155:8453`).
- **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.
- **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.
- **Security:** wallet private keys never touch the frontend; only the public client key is embedded. Session tokens are minted server-side.

Try it: open `https://your-host/v1/quote` in Chrome after configuring the keys above.

---

## What it does

```
Agents / clients
   ├─ HTTP  /v1/*          → x402 402 JSON (agents) or Smart Wallet HTML paywall (browsers)
   ├─ GET   /v1/fetch-md   → paid HTML→Markdown demo (same x402 gate)
   ├─ GET   /x402/discover → free agent yellow pages (alias /discover; from merchants JSON)
   ├─ GET   /.well-known/x402.json → free Bazaar-friendly origin manifest (alias /.well-known/x402)
   ├─ GET   /.well-known/agent-card.json → free agent card (alias /.well-known/agent.json)
   ├─ GET   /llms.txt      → free AI-crawler summary (alias /.well-known/llms.txt)
   ├─ GET   /openapi.yaml  → free OpenAPI 3.1 (alias /docs/openapi.yaml)
   ├─ POST  /x402/session-token → free Onramp session token (when CDP server keys set)
   ├─ GET   /merchants     → free merchant registry (id, label, seller, payTo)
   ├─ GET   /health        → free (+ paywall config flags)
   ├─ GET   /              → developer landing (EN / 中文)
   └─ MCP   /mcp           → server_info (free), get_quote + proxy_request + fetch_md (paid + bazaar)
```

| Surface | Stack |
|---|---|
| HTTP | `createX402Server` + `paymentMiddlewareFromHTTPServer` + `@x402/paywall` (browser) |
| MCP | `x402ResourceServer` + `createCdpFacilitatorClient` + `createPaymentWrapper` + Bazaar extension |

CLI: `npx x402-micro-tollgate@0.3.3` | `--seller 0x…` / `-s` | `--stdio` | `--port N`

Agent SEO: [`llms.txt`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/llms.txt) · [`docs/openapi.yaml`](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/docs/openapi.yaml)

---

## Publishing

Package identity (must stay in sync):

- `package.json` `mcpName` = `io.github.kevin2003050666-coder/x402-micro-tollgate`
- `server.json` `name` = same string

Flow (needs tokens on a machine that has them — not this VM):

1. Public GitHub mirror at `kevin2003050666-coder/x402-micro-tollgate`
2. `npm publish` (or tag `v*` → `.github/workflows/publish-mcp.yml`)
3. `mcp-publisher publish` (OIDC) → official MCP Registry
4. PulseMCP auto-ingests from the official registry — **do not** submit to PulseMCP directly

Update `server.json` `remotes[0].url` to your real public `/mcp` before publishing remotes.

---

## Tests

```bash
npm test
```

No live CDP credentials required.

---

## License

MIT · See [CONTRIBUTING.md](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/CONTRIBUTING.md) · [SECURITY.md](https://github.com/kevin2003050666-coder/x402-micro-tollgate/blob/HEAD/SECURITY.md)

