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). Repo: github.com/kevin2003050666-coder/x402-micro-tollgate · Questions: 2767111713@qq.com
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 — 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) |
| 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/.
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
- Price tag — unpaid traffic gets HTTP 402 with the amount due (conversion, not rejection)
- Pay — the agent settles a USDC micropayment in seconds via Coinbase CDP
- 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+.
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):
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 (also GET /llms.txt and GET /.well-known/llms.txt when the gateway is running) · OpenAPI 3.1: docs/openapi.yaml (also GET /openapi.yaml and GET /docs/openapi.yaml)
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:
{
"mcpServers": {
"x402-micro-tollgate": {
"url": "http://127.0.0.1:8402/mcp"
}
}
}
Stdio:
{
"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)
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 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).
npm i x402-micro-tollgate@0.3.3
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.
# 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
Uses render.yaml: Node 22, npm start, health /health, env names from .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 (force Render GitHub auto-deploy so production picks up GET /x402/discover). 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 / 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; do not hardcode secrets) |
FEE_FREE_BELOW_USDC / X402_FEE_FREE_BELOW_USDC | 10000000 | Atomic USDC (6 decimals). < $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 > 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 |
|---|---|
< 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), 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 (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:
- Deploy
FeeSplitterwithseller= merchant wallet,feeCollector=0xa922F38041B5ee227c96A547F106F1330447e30E,feeBps= 10, and the chain’s native USDC asasset. - Add an entry to
MERCHANTS_JSON(ormerchants.json/ copy frommerchants.example.json):{ "acme": { "seller": "0xMerchantWallet…", "payTo": "0xDeployedFeeSplitter…", "label": "Acme API" } } - Call gated APIs with
?merchant=acmeor headerx-merchant-id: acme(case-insensitive). Agents SHOULD always send merchant id. If omitted, the gateway falls back toDEFAULT_MERCHANT(demo) — that means traffic (and USDC) can silently land on the demo FeeSplitter. SetREQUIRE_MERCHANT=trueto reject missing merchant with400 { "error": "merchant_required" }. Unknown merchant on gated paths →400{ "error": "unknown_merchant" }. - Free listing:
GET /merchants(also/v1/merchants). - Agent yellow pages:
GET /x402/discover(aliasGET /discover) — stable JSON catalog derived from the same registry /SELLER/ demo. JSON config stays supported; there is no on-chainRegistry.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. 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:
{
"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. 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:
- 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 get409 { "error": "payment_already_used" }. Durable mark happens only after settle success; verify failure releases the pending reservation so honest retries are not bricked. - SSRF on
/v1/fetch-md— Onlyhttp/https; DNS resolve rejects private/bogon/link-local/metadata; DNS is re-checked before fetch (rebinding defense); redirects are disabled. - Upstream bypass — Optional
UPSTREAM_SHARED_SECRET: after payment, proxy injectsX-Tollgate-Secret/X-Tollgate-Paid/X-Tollgate-Timestamp. Your upstream must require them (see snippet below). This is a shared-secret MVP, not mTLS. - 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 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)
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. 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 is the x402 discovery catalog.
- HTTP:
createX402Serverauto-injects bazaar; we override withdiscoverable: true, descriptions, and input/output schemas (GET /v1/quote+ gated prefix proxies). - MCP:
x402ResourceServerdoes not auto-declare Bazaar — we registerbazaarResourceServerExtensionand passdeclareDiscoveryExtension({ toolName, inputSchema, … })onget_quote/proxy_request. Resource URL isPUBLIC_BASE_URL/mcp(a real http(s) URL, not a display name).
To appear in Bazaar:
- Set
PUBLIC_BASE_URLto a public https origin (localhost listings are a no-op for real crawlers). - Run with live CDP credentials +
X402_PAY_TO. - 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
- Connect / create a Coinbase Smart Wallet via the official
@x402/paywallEVM UI (Coinbase Wallet connector — Passkey Smart Wallet, no MetaMask required). - Sign the x402
exactEIP-3009 payment on Base (or Base Sepolia in development) and retry withPAYMENT-SIGNATURE. - Get USDC (optional): when server CDP keys are set, the paywall shows a Get USDC button that calls
POST /x402/session-tokenand opens Coinbase Onramp. 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)
# .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 strictscript-srcwithout 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 /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 (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 · docs/openapi.yaml
Publishing
Package identity (must stay in sync):
package.jsonmcpName=io.github.kevin2003050666-coder/x402-micro-tollgateserver.jsonname= same string
Flow (needs tokens on a machine that has them — not this VM):
- Public GitHub mirror at
kevin2003050666-coder/x402-micro-tollgate npm publish(or tagv*→.github/workflows/publish-mcp.yml)mcp-publisher publish(OIDC) → official MCP Registry- 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
npm test
No live CDP credentials required.
License
MIT · See CONTRIBUTING.md · SECURITY.md