The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the X402 Micro Tollgate listing page.
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”).
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.
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/.
| 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) |
No protocol monthly fee. Monetization is the toll — like interchange on card rails.
Requires Node.js 22+.
npx one-liners (no clone):
--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) · OpenAPI 3.1: 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:
Stdio:
(X402_PAY_TO in env still works if you omit --seller.)
Or set SELLER / X402_SELLER in .env and run npm start. See Environment for the $10 threshold, factory address, and fee-release details.
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).
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.
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.
| 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.
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 + 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 / docker-compose.yml).
| 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 |
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.
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:
FeeSplitter with seller = merchant wallet, feeCollector = 0xa922F38041B5ee227c96A547F106F1330447e30E, feeBps = 10, and the chain’s native USDC as asset.MERCHANTS_JSON (or merchants.json / copy from merchants.example.json):
?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" }.GET /merchants (also /v1/merchants).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. This is receive → later release(), not an atomic same-transaction split and not OpenZeppelin PaymentSplitter.
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:
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。
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.
Four defenses buyers and sellers should know about:
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./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.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.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).
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.
Or import verifyUpstreamTrustHeaders from x402-micro-tollgate in your upstream process.
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 is the x402 discovery catalog.
createX402Server auto-injects bazaar; we override with discoverable: true, descriptions, and input/output schemas (GET /v1/quote + gated prefix proxies).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).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:
PUBLIC_BASE_URL to a public https origin (localhost listings are a no-op for real crawlers).X402_PAY_TO.Launch tip: share your /mcp or /v1/quote URL in Discord #x402 after that first settlement.
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.
@x402/paywall EVM UI (Coinbase Wallet connector — Passkey Smart Wallet, no MetaMask required).exact EIP-3009 payment on Base (or Base Sepolia in development) and retry with PAYMENT-SIGNATURE.POST /x402/session-token and 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.| 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 } |
development) and Base mainnet (production / NETWORK=eip155:8453).@x402/paywall). Prefer not to set a strict script-src without nonces on 402 HTML responses.Try it: open https://your-host/v1/quote in Chrome after configuring the keys above.
| 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
Package identity (must stay in sync):
package.json mcpName = io.github.kevin2003050666-coder/x402-micro-tollgateserver.json name = same stringFlow (needs tokens on a machine that has them — not this VM):
kevin2003050666-coder/x402-micro-tollgatenpm publish (or tag v* → .github/workflows/publish-mcp.yml)mcp-publisher publish (OIDC) → official MCP RegistryUpdate server.json remotes[0].url to your real public /mcp before publishing remotes.
No live CDP credentials required.
MIT · See CONTRIBUTING.md · SECURITY.md