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: OpenAPI MCP Server Converter
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 — top-up → invoke → spend log (no
EVM_PRIVATE_KEYon the happy path)Settle-on-success debit — hold balance, proxy upstream, finalize debit only on 2xx
Seller 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 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 |
|
|
|
|
|
|
| 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=0xYourRecipient
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=0xYourRecipient
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=0xYourPayerKey
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
Create hosted MCP servers from any OpenAPI spec. Requires a free Kaiva Bridge account.
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.
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.4 npmMIT
- AlicenseNot gradedqualityCmaintenanceAutomatically converts OpenAPI specifications into a Model Context Protocol (MCP) server instance.221 npm16MIT
- 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.25 npm1MIT