PayMCP
Integrates with Coinbase CDP x402 facilitator for payment settlement on Base mainnet, enabling paid MCP server requests via HTTP 402 and x402 protocol.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@PayMCPConvert my petstore openapi.yaml into a paid MCP server"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
PayMCP
Turn an existing OpenAPI 3.x spec into a paid MCP server with HTTP 402 + x402 settlement.
npm: openapi-to-paymcp · CLI: paymcp · repo: ranjan2829/PayMCP
Why / What it is
PayMCP is an adapter, not a new payment protocol. It compiles your OpenAPI operations into:
a paid MCP server (tools that challenge, verify, call upstream, then settle on 2xx)
a Fastify paywall (HTTP 402 + x402 headers on paid routes)
a settlement ledger (SQLite by default, optional Postgres)
Settlement always goes through a real HTTP facilitator (POST /verify + POST /settle). There is no FakeSettler and no simulated settlement product mode.
Related MCP server: MyMCP
Quickstart
npm i -g openapi-to-paymcp
paymcp ./openapi.yaml --out ./paid-server
# or one-shot:
npx openapi-to-paymcp ./openapi.yaml --out ./paid-serverSet the required env vars (see Configuration), then run the generated server. Copy .env.example as a starting point — never commit .env.
How it works
sequenceDiagram
participant Client
participant PayMCP as PayMCP paywall / MCP
participant Fac as x402 facilitator
participant Up as Upstream OpenAPI API
Client->>PayMCP: request (no payment)
PayMCP-->>Client: 402 + PAYMENT-REQUIRED
Client->>PayMCP: request + PAYMENT-SIGNATURE
PayMCP->>Fac: POST /verify
Fac-->>PayMCP: ok
PayMCP->>Up: execute upstream / handler
Up-->>PayMCP: 2xx success
Note over PayMCP: Settle only after 2xx (not on entry / 4xx / 5xx)
PayMCP->>Fac: POST /settle
Fac-->>PayMCP: SettlementResponse
PayMCP->>PayMCP: ledger (idempotent)
PayMCP-->>Client: 200 + PAYMENT-RESPONSEASCII equivalent:
Client ──402──► PayMCP (paywall / MCP)
│ PAYMENT-REQUIRED → client signs
│ PAYMENT-SIGNATURE → verify (early)
▼
upstream / handler
│ 2xx only → FacilitatorSettler.settle
│ 4xx/5xx → do NOT settle (ledger failed)
▼
ledger (SQLite | Postgres) + PAYMENT-RESPONSESettlement timing: FacilitatorSettler.settle runs only after a successful upstream/handler response (2xx), not on tool-call or paywall entry. Verify may run early; failed upstream responses never settle. Already-settled Idempotency-Keys skip verify/settle; in-flight keys fail closed (409).
Path | Role |
| Publishable CLI + library |
| Agent-trace quality gate (CI harness) |
| Paid-agent-tools marketplace (listings, credit ledger, invoke) |
| Sample Fastify API with |
| Buyer example using |
| Protocol fixture demo (not live money) |
| Live settle (gated by |
PayMCP Store (@paymcp/store)
Mini paid-agent-tools marketplace on top of this adapter:
Listings — OpenAPI-backed tool catalog with atomic USDC/credit prices
Buyer credits — Stripe Checkout → verified webhook → invoke → spend log (no buyer
EVM_PRIVATE_KEY)Settle-on-success debit + seller payout — hold balance, proxy upstream, finalize debit and pay listing
payToon 2xxSeller kit — OpenAPI + prices → compile ops → register listing
Seed — demo echo/weather + live x402 docs for
https://grawwww.xyz/api/render/image(0.10 USDC)
pnpm --filter @paymcp/store seed
pnpm --filter @paymcp/store dev # http://127.0.0.1:8790
pnpm --filter @paymcp/store cli fund-checkout buyer_demo 100
# complete Stripe Checkout; webhook credits ledger; then:
pnpm --filter @paymcp/store cli buyer-flow buyer_demoFull docs: packages/store/README.md. Store env keys are in .env.example (STORE_*).
Configuration
Settlement always targets a real facilitator base URL. Boot is fail-fast (zod): missing/invalid facilitator, payTo, network, or asset aborts with a clear multi-line error.
Required
Variable | Example |
|
|
| EVM recipient address (set in env; no placeholder) |
|
|
| USDC contract on that network |
Optional
Variable | Purpose |
| Bearer/JWT for CDP facilitator |
| Operator docs (mint token separately; not required at runtime if you set the auth token) |
| Default |
| Accept window |
|
|
| SQLite path (default |
|
|
| Default |
| 5xx/network only (default |
| Paid-route rate limit ( |
| Default |
| HMAC secret for |
| Billing webhook URL (optional; omit to disable) |
| HMAC secret for |
| Webhook HTTP timeout (default |
| 5xx/network retries for webhook (default |
Demo (fixture) vs live money
Protocol fixture demo — not live money
pnpm demo drives unpaid → 402 → PAYMENT-SIGNATURE → 200 + PAYMENT-RESPONSE using the real FacilitatorSettler HTTP client against a recorded facilitator protocol fixture (same JSON shapes as production /verify and /settle). No on-chain funds move.
pnpm build
pnpm demoLive money — Base Sepolia, then Base mainnet
Use a real facilitator and a real wallet. Prefer Sepolia first.
1) Base Sepolia (recommended first)
Setting | Value |
Facilitator |
|
Network |
|
USDC |
|
EIP-712 name / version |
|
export PAYMCP_FACILITATOR_URL=https://x402.org/facilitator
export PAYMCP_PAY_TO= # set real 0x recipient in your shell / .env
export PAYMCP_NETWORK=eip155:84532
export PAYMCP_ASSET=0x036CbD53842c5426634e7929541eC2318f3dCF7e
pnpm --filter @paymcp/demo-api build
pnpm --filter @paymcp/demo-api start
# GET /healthz GET /readyz2) Base mainnet (CDP)
Setting | Value |
Facilitator |
|
Network |
|
USDC |
|
EIP-712 name / version |
|
Auth |
|
Wallet / payer (PAYMENT-SIGNATURE) — do not invent signing crypto. Use the official x402 client:
Docs: Quickstart for buyers
Packages:
@x402/fetch^2.25.0,@x402/evm^2.25.0(ExactEvmScheme),viemaccountsSpec: exact EVM / EIP-3009
Runnable example:
examples/buyer(pnpm buyer:example)
import { wrapFetchWithPayment, x402Client } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
const signer = privateKeyToAccount(process.env.EVM_PRIVATE_KEY!);
const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(signer));
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const res = await fetchWithPayment("http://127.0.0.1:8787/echo", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ message: "hello" }),
});Buyer example (@x402/fetch)
This is an example, not a new protocol. wrapFetchWithPayment handles unpaid → 402 + PAYMENT-REQUIRED → sign → retry with PAYMENT-SIGNATURE. The PayMCP server still verifies and settles through its real facilitator after a 2xx handler. The buyer client does not call /verify or /settle.
Set PAYMCP_ASSET_NAME=USDC on the server so the 402 extra.name / extra.version fields match EIP-3009 domain data.
# terminal 1: local paymcp demo-api (real facilitator)
export PAYMCP_FACILITATOR_URL=https://x402.org/facilitator
export PAYMCP_PAY_TO= # set real 0x recipient in your shell / .env
export PAYMCP_NETWORK=eip155:84532
export PAYMCP_ASSET=0x036CbD53842c5426634e7929541eC2318f3dCF7e
export PAYMCP_ASSET_NAME=USDC
pnpm --filter @paymcp/demo-api build
pnpm --filter @paymcp/demo-api start
# terminal 2: inspect 402 (no wallet, no funds)
pnpm buyer:probe
# terminal 2: pay /echo (spends testnet USDC; refuses unless PAYMCP_LIVE=1)
export PAYMCP_LIVE=1
export EVM_PRIVATE_KEY= # set real key in your shell / .env — never commit
export DEMO_API_URL=http://127.0.0.1:8787
pnpm buyer:exampleEnv names: .env.example. Prefer Base Sepolia. Never commit .env.
Live settle script (refuses unless PAYMCP_LIVE=1; never prints the full signature):
# terminal 1: demo-api with real env
pnpm --filter @paymcp/demo-api start
# terminal 2:
PAYMCP_LIVE=1 \
PAYMENT_SIGNATURE_B64=... \
DEMO_API_URL=http://127.0.0.1:8787 \
node scripts/live-settle.mjsx402 V2 headers
Header | Direction | Body |
| server → client | base64 |
| client → server | base64 |
| server → client | base64 |
| client → server | opaque string (optional; derived from signature if omitted) |
Idempotent retries: Send the same Idempotency-Key when agents retry a paid request. After a successful settle, PayMCP returns the prior PAYMENT-RESPONSE and does not call the facilitator again. Concurrent duplicates while a settle is in flight get 409 idempotency_in_flight (fail closed — prefer this over waiting). Different keys settle independently. Applies to both the HTTP paywall and MCP tool paths.
Allowlist + per-tool budgets
Production controls for which tools can be paid/exposed and how much they may settle per day.
Allowlist
When configured, only listed operationIds may be paid or exposed. Others get 403 (operation_not_allowlisted) on the HTTP paywall and a clear MCP tool error.
Source | Example |
Env |
|
|
|
| same |
CLI |
|
Precedence: env > CLI --allow > prices.yaml > budgets.yaml. If unset, all compiled ops are allowed (backward compatible).
Per-tool daily budgets
Hard stop on settled spend tracked from the ledger (status settled only). When spent + requested > max, the request fails with 429 (budget_exceeded) and settle is not called.
# prices.yaml (per-op) or budgets.yaml
version: 1
allowlist: [echoMessage, getWeather]
window: calendar_day_utc # or rolling_24h
defaultMaxDailyAtomic: "100000"
operations:
- operationId: echoMessage
amount: "10000"
maxDailyAtomic: "50000"
- operationId: getWeather
amount: "25000"
maxDailyAtomic: "75000"
# optional per-tenant overrides (budgets.yaml):
# tenants:
# - tenantId: acme
# defaultMaxDailyAtomic: "200000"
# operations:
# - operationId: echoMessage
# maxDailyAtomic: "30000"Env | Purpose |
| Global default cap (atomic units) |
|
|
| Path to |
Optional tenant: HTTP header x-paymcp-tenant or MCP argument tenantId. Budgets are independent per tool (and per tenant when set). Both the HTTP paywall and MCP paths enforce allowlist + budgets. Settlement still uses real FacilitatorSettler only after 2xx, with idempotent retries unchanged.
Settlement webhooks (billing)
After a successful settle (handler/upstream 2xx and facilitator success: true), PayMCP can POST a signed JSON event to your billing endpoint. Failed settles, non-2xx handlers, and idempotent replays do not fire the webhook.
Configure
export PAYMCP_WEBHOOK_URL=https://billing.example/webhooks/paymcp
export PAYMCP_WEBHOOK_SECRET='your-long-random-secret' # ≥16 chars
# optional:
# export PAYMCP_WEBHOOK_TIMEOUT_MS=5000
# export PAYMCP_WEBHOOK_MAX_RETRIES=2Request
POSTwithContent-Type: application/jsonHeader
X-PayMCP-Signature: sha256=<hex>— HMAC-SHA256 of the raw body usingPAYMCP_WEBHOOK_SECRETBody fields:
event(settlement.succeeded),version,operationId,amount,network,asset,payer,transaction,idempotencyKey,settledAt, optionalrequestId
Never included: raw PAYMENT-SIGNATURE / PaymentPayload. Delivery failures are logged and retried (5xx/network) but do not fail the client settle response. De-dupe on idempotencyKey if you need strict once-only billing.
Verify (billing side)
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyPaymcpWebhook(rawBody: string, signatureHeader: string, secret: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader);
return a.length === b.length && timingSafeEqual(a, b);
}Signed dispute / evidence packs
Export a chargeback-ready evidence pack from the settlement ledger (settled rows only). Packs are content-hashed and signed with HMAC-SHA256 so operators can prove integrity when responding to disputes.
CLI
export PAYMCP_DISPUTE_HMAC_SECRET='your-long-random-secret'
# optional: PAYMCP_LEDGER_PATH=./paymcp-ledger.db
# optional: PAYMCP_DATABASE_URL=postgres://…
paymcp dispute-pack \
--from 2026-09-01T00:00:00.000Z \
--to 2026-09-12T23:59:59.999Z \
--out pack.jsonLibrary
import {
createLedger,
exportDisputePack,
verifyDisputePackSignature,
} from "openapi-to-paymcp";
const ledger = await createLedger({ ledgerPath: "./paymcp-ledger.db" });
const pack = await exportDisputePack({
ledger,
from: "2026-09-01T00:00:00.000Z",
to: "2026-09-12T23:59:59.999Z",
hmacSecret: process.env.PAYMCP_DISPUTE_HMAC_SECRET!,
});
await ledger.close();
const ok = verifyDisputePackSignature(pack, process.env.PAYMCP_DISPUTE_HMAC_SECRET!);Pack contents
Field | Description |
| Settled rows: |
| Evidence scope + schema version |
| SHA-256 of canonical JSON of the unsigned body |
|
|
Never included: full PAYMENT-SIGNATURE / PaymentPayload bodies (or other long base64 payment blobs). Set PAYMCP_DISPUTE_HMAC_SECRET (≥16 characters); keep it out of git.
Security practices
Fail-closed settle — network errors, verify rejects, and settle failures never succeed the request
Redacted logs —
PAYMENT-SIGNATUREandAuthorizationare never logged in fullDispute packs — exports omit
PAYMENT-SIGNATUREpayloads; HMAC (PAYMCP_DISPUTE_HMAC_SECRET) bindscontentHashSettlement webhooks — optional billing notify after successful settle; HMAC
X-PayMCP-Signature; noPAYMENT-SIGNATUREin payloadNo secrets in the package —
.envis gitignored; publish includes onlydist,bin, docsIdempotency-Key — same key never double-charges: settled rows replay prior
PAYMENT-RESPONSE(skip verify/settle); in-flightpendingfails closed with409 idempotency_in_flight(no second settle); SQLite/PostgresUNIQUE(idempotency_key)ensures only one concurrent settle winsOptional rate limit —
PAYMCP_RATE_LIMIT_MAXon paid routesRequest IDs —
x-request-idon every request (demo-api)
See SECURITY.md for how to report vulnerabilities.
Library API
npm i openapi-to-paymcpimport {
paymcpPaywall,
loadConfigFromEnv,
buildPriceTable,
loadPricesFile,
compileOperations,
loadOpenApi,
FacilitatorSettler,
createPaidMcpServer,
} from "openapi-to-paymcp";CLI from the monorepo:
pnpm exec paymcp ./examples/demo-api/openapi.yaml --out ./paid-server
pnpm exec paymcp ./examples/demo-api/openapi.yaml \
--prices ./examples/demo-api/prices.yaml \
--serve --upstream http://127.0.0.1:8787Agent skill notes: packages/paymcp/SKILL.md.
Monorepo develop / Docker / CI
pnpm install
pnpm build
pnpm typecheck
pnpm test
pnpm harness:ci # agent-trace quality gate (good/bad fixtures)cp .env.example .env # fill required vars — never commit .env
docker compose up --build
curl -s http://127.0.0.1:8787/healthzCI (GitHub Actions): install → typecheck → build → test on Node 20.
Harness CI (agent-trace gate)
packages/harness-ci evaluates JSON agent/tool traces against PayMCP production rules and fails the PR when a fixture expectation is wrong:
Rule | Meaning |
| Paid requests / settle must carry |
| Settle only after a 2xx handler reply |
| Same key must not settle successfully twice |
| Must not allow spent+requested over daily max |
| No raw |
pnpm harness:ci # run good/ + bad/ fixtures
pnpm harness:ci -- --file path/to.json # evaluate one trace
pnpm harness:ci -- --list-rulesWorkflow: .github/workflows/harness-ci.yml (runs on every PR alongside ci.yml).
Publish (openapi-to-paymcp)
Root monorepo stays private. Only packages/paymcp publishes.
pnpm --filter openapi-to-paymcp publish --access publicprepublishOnly runs the TypeScript build.
Contributing
See CONTRIBUTING.md. Bug reports and focused PRs welcome.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Monetize any MCP server: x402 paywall, pay-per-call billing in USDC on Base, agent marketplace.
Pay-per-action access to APIs and MCP tools over Lightning L402 and Base USDC x402.
Billing proxy for MCP servers. Adds Stripe and x402 crypto payments without writing billing code.
- TypeshipOAuthdev.typeship
Generate a typed SDK, CLI, and MCP server from any OpenAPI or GraphQL spec, and keep them current.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceConverts any OpenAPI 3.x specification into a fully functional MCP server with OAuth 2.1 support and a purely functional architecture.8 npmMIT
- AlicenseNot gradedqualityBmaintenanceDynamically converts any OpenAPI v3 specification into a fully-functional Model Context Protocol (MCP) server.7Mozilla Public 2.0
- AlicenseNot gradedqualityCmaintenanceTurn any OpenAPI / Swagger spec into an agent-ready MCP server.22 npm1MIT
- AlicenseNot gradedqualityCmaintenanceConvert any OpenAPI spec into a secure MCP server with scoped auth, per-tool allow/deny policies, rate limiting, and a redacted audit trail.10 npmMIT