Back to Browse

X402 Micro Tollgate MCP Server

Developer ToolsLow Risk10.0MCP RegistryLocal
Free

Server data from the Official MCP Registry

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

About

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

Security Report

10.0
Low Risk10.0Low Risk

Valid MCP server (1 strong, 0 medium validity signals). 1 known CVE in dependencies Package registry verified. Imported from the Official MCP Registry. Trust signals: trusted author (3/3 approved).

5 files analyzed · 2 issues found

Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.

What You'll Need

Set these up before or after installing:

Coinbase CDP API key ID for the x402 facilitatorRequired

Environment variable: CDP_API_KEY_ID

Coinbase CDP API key secret for the x402 facilitatorRequired

Environment variable: CDP_API_KEY_SECRET

EVM address that receives USDC settlementsOptional

Environment variable: X402_PAY_TO

Upstream HTTP API to proxy after payment (omit for built-in mock)Optional

Environment variable: UPSTREAM_URL

Dollar price per paid call, e.g. $0.001Optional

Environment variable: PRICE

CAIP-2 network id (eip155:84532 Base Sepolia or eip155:8453 Base)Optional

Environment variable: NETWORK

development or productionOptional

Environment variable: X402_ENVIRONMENT

How to Install

Add this to your MCP configuration file:

{
  "mcpServers": {
    "io-github-kevin2003050666-coder-x402-micro-tollgate": {
      "env": {
        "PRICE": "your-price-here",
        "NETWORK": "your-network-here",
        "X402_PAY_TO": "your-x402-pay-to-here",
        "UPSTREAM_URL": "your-upstream-url-here",
        "CDP_API_KEY_ID": "your-cdp-api-key-id-here",
        "X402_ENVIRONMENT": "your-x402-environment-here",
        "CDP_API_KEY_SECRET": "your-cdp-api-key-secret-here"
      },
      "args": [
        "-y",
        "x402-micro-tollgate"
      ],
      "command": "npx"
    }
  }
}

Documentation

View on GitHub

From the project's GitHub 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). 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.

NetworkCAIP-2USDCUSDTStatusFacilitator / notes
Baseeip155:8453native CirclebridgedliveCDP exact. FeeSplitterFactory live (deployments/base.json)
Base Sepoliaeip155:84532test USDCliveCDP testnet
Optimismeip155:10nativebridgedconfig-readyAddresses wired; not on current CDP matrix
Arbitrum Oneeip155:42161nativebridgedliveCDP exact; factory stub
Polygon PoSeip155:137native (not USDC.e)PoS USDTliveCDP exact; factory stub
BNB Smart Chaineip155:56peg 18 decpeg 18 decconfig-readyNot on CDP list
Ethereumeip155:1nativeTetherconfig-readyFacilitator-dependent
Avalanche C-Chaineip155:43114nativeUSDTconfig-readyIn @x402/evm defaults
Celo / Seieip155:42220 / 1329USDCconfig-readyCatalog extras
Solanasolana:5eykt…SPL USDCSPL USDTexperimentalCDP lists Solana; gateway stubs + optional SVM paywall (SOLANA_PAY_TO)
TRONtron:mainnetTRC-20plannedNo 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

WalletRole
Coinbase Smart Wallet (Passkey)Primary CTA
MetaMaskVia wagmi injected target
InjectedOther browser wallets
WalletConnectWhen WALLETCONNECT_PROJECT_ID set
Solana paywallExperimental, behind Solana accepts / PAYWALL_SVM
TronLinkNot 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+.

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) · 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:

{
  "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_maxAgent 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
EnvDefaultNotes
BUYER_PRIVATE_KEY(required)0x… EOA used to sign EIP-3009
TARGET_URLhttps://x402-micro-tollgate.onrender.com/v1/quotePaid route to hit
MAX_SINGLE_USDC0.05Per-call cap (maxSingleSpendUsdc)
MAX_TOTAL_USDC1.00Session cap / human B_max
ROUNDS3Stop 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

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


Environment

VariableDefaultNotes
PORT8402HTTP listen port
UPSTREAM_URL(unset → mock)Your API origin
UPSTREAM_SHARED_SECRET / X402_UPSTREAM_SECRETOptional. After settle, tollgate injects X-Tollgate-Secret + X-Tollgate-Paid (HMAC). Upstream must require them and reject public direct hits (not mTLS)
X402_PAY_TOEVM receive address (live mode SDK init)
SELLER / X402_SELLERPermissionless seller EOA (EIP-55 validated; invalid → startup fail)
FACTORY_ADDRESSOperator-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_USDC10000000Atomic USDC (6 decimals). < $10 → payTo=seller; ≥ $10 → FeeSplitter
CDP_API_KEY_ID / CDP_API_KEY_SECRETCDP facilitator (+ Onramp session tokens)
CDP_CLIENT_API_KEYPublic CDP client key for browser Smart Wallet paywall (safe in frontend). Alias: CDP_CLIENT_KEY
X402_FACILITATOR_URL / CDP_FACILITATOR_URLCDP defaultOptional alternate facilitator base URL for MCP (createCdpFacilitatorClient({ baseUrl })). Single-vendor; no multi-facilitator routing yet
PRICE$0.001Network default USDC/USDT price tag
NETWORK / X402_NETWORKeip155:84532 / 8453Primary CAIP-2 (aliases: base, optimism, …)
NETWORKS / X402_NETWORKS(primary only)Comma/JSON list of networks for multi-accept
ASSETS / X402_ASSETSUSDCCross with NETWORKS → accepts (USDC, USDT)
ACCEPTS_JSON / X402_ACCEPTS_JSONExplicit accepts array (overrides NETWORKS×ASSETS)
FACTORY_ADDRESSESJSON map caip2 → FeeSplitterFactory (Base live; others optional)
SOLANA_PAY_TOBase58 payTo for experimental Solana accepts
PAYWALL_SVMfalseForce @x402/paywall SVM UI (also auto if Solana in accepts)
WALLETCONNECT_PROJECT_IDEnables WalletConnect in browser paywall
X402_MIN_PRICE_USDCOptional static minimum accept amount (atomic USDC). Effective price = max(PRICE, this, gas floor)
X402_DYNAMIC_MIN_ENABLEDfalseWhen true, Base base-fee oracle may bump min accept / feeFreeBelow if gas would eat too much of the payment
X402_GAS_COST_MAX_FRACTION0.5Bump when estimated gas USD > this fraction of the payment
X402_GAS_ORACLE_TTL_MS30000baseFee cache TTL (clamped 15–60s)
X402_GAS_RPC_URL / BASE_RPC_URLpublic Base RPCJSON-RPC for eth_getBlockByNumber baseFee
X402_GAS_USED_ESTIMATE100000Rough L2 gas units for settle cost estimate
X402_ETH_USD4000Conservative ETH/USD floor (no live FX required)
X402_ENVIRONMENTdevelopmentor production
GATED_PREFIX/v1HTTP paths that require payment
PUBLIC_BASE_URLhttp://127.0.0.1:$PORTPublic https:// origin for Bazaar resource URLs
CONTACT_EMAIL2767111713@qq.comLanding contact mailto (not a SaaS CTA)
FEE_BPS10Documented operator fee (0.1%). Live settle still pays 100% to payTo until release()
FEE_COLLECTOR0xa922…7e30EFixed operator wallet for the 0.1% slice after FeeSplitter.release()
MERCHANTS_JSONOptional hosted multi-tenant registry (not required when SELLER is set)
MERCHANTS_FILEmerchants.jsonFile path; falls back to merchants.example.json / built-in demo when no seller
DEFAULT_MERCHANTdemoUsed when ?merchant= / x-merchant-id omitted — agents SHOULD always send merchant id
REQUIRE_MERCHANTfalseWhen true, gated paths reject missing merchant id with 400 {error:"merchant_required"} (no demo fallback)
PAYMENT_DEDUPE_TTL_MS600000In-memory payment-proof idempotency window (10 min). CDP facilitator nonce remains source of truth
PAYMENT_DEDUPE_MAX_ENTRIES10000Max keys in the gateway PaymentDedupeStore LRU (no Redis; interface is Redis-ready)
X402_VERIFY_TIMEOUT_MS15000Documented verify-phase budget (local / facilitator verify)
X402_SETTLE_TIMEOUT_MS180000Wait for facilitator/on-chain settle before 202 payment_pending (default 3 min — Base congestion)
KEEPER_ENABLEDfalseOptional FeeSplitter release() keeper — never on by default
KEEPER_DRY_RUN(see notes)true logs keeper_would_release without sending txs
KEEPER_PRIVATE_KEYEVM key for live release() only (never commit)
KEEPER_RPC_URLBase public RPCJSON-RPC endpoint for balance + release
KEEPER_INTERVAL_MS3600000Poll interval (1h)
KEEPER_MIN_USDC1000000Min 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:

  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):
    {
      "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. 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):

TrackWhy 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 /healthgasFloor.

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 latencyX402_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 pendingsettledconsumed:

Store stateClient 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
consumed409 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: 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 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. 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)
PieceRole
CDP_CLIENT_API_KEYInjected into paywall HTML as window.x402.cdpClientKey
CDP_API_KEY_ID + CDP_API_KEY_SECRETFacilitator settle and POST /x402/session-token (Onramp JWT)
POST /x402/session-tokenFree 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)
SurfaceStack
HTTPcreateX402Server + paymentMiddlewareFromHTTPServer + @x402/paywall (browser)
MCPx402ResourceServer + 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.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

npm test

No live CDP credentials required.


License

MIT · See CONTRIBUTING.md · SECURITY.md

Reviews

No reviews yet

Be the first to review this server!