@deepfirstsearch/agent-pay-mcp
v0.4.0
Published
MCP server: any MCP agent (Claude, Cursor...) pays x402 APIs inside an owner-signed, on-chain budget
Maintainers
Readme
@deepfirstsearch/agent-pay-mcp
An MCP server that lets any MCP agent (Claude Desktop, Claude Code, Cursor, …) pay x402 APIs in USDC on Base without being able to overspend, even if a web page or API response prompt-injects it.
Beta, unaudited. Use Base Sepolia or small amounts on Base mainnet.
The model gets three tools and nothing else:
| Tool | What the model can do |
|---|---|
| paid_fetch(url, method?, body?, contentType?) | Fetch a URL. If it answers 402 Payment Required, the payment goes through only if the merchant, price and budget match your config |
| check_price(url, method?, body?, contentType?) | Free: what paying this URL would do, without paying. The price and payee it would pay, or why it would refuse (payee_mismatch, price_changed, …). Nothing is signed. no_challenge means no terms were shown right now, not that the route is free |
| list_merchants() | See which origins it may pay, their prices and what's left |
| budget_status() | Remaining budget per merchant, window renewal, vault balance |
The model cannot choose a payee, an amount, a network or a limit: there is no argument for any of them. Merchants, price pins, caps and the spending plan come from your config file; keys come from environment variables. Responses are returned inside a randomly tagged fence marked as untrusted data. On-chain, the Agent Safe vault enforces your signed per-payment and per-day caps even if this machine is compromised.
Setup
- Budget (owner, once): create a vault and sign a budget per merchant with the owner CLI:
npx @deepfirstsearch/agent-pay owner create-vault, thenowner budget …, which prints theintentIdand a ready-to-pastemerchants[]entry. (No vault? Leave outvault,trancheandintentId; each merchant then gets its own payer address derived fromAGENT_PAY_BURNER_SEED, which you fund with USDC yourself.) - Config: copy
config.example.jsonand fill it in. Amounts are USDC decimal strings. Keeptrancheat or below each intent'strancheCapandmaxPerTx. - Secrets (environment only):
AGENT_PAY_AGENT_KEY: the intent's agent key (only with a vault).AGENT_PAY_MCP_CONFIG: the config path, if you don't pass it as an argument.AGENT_PAY_BURNER_SEED: the 32-byte secret the payer addresses were derived from. Never your owner key.
Claude Code
claude mcp add agent-pay \
-e AGENT_PAY_AGENT_KEY=0x… -e AGENT_PAY_BURNER_SEED=0x… \
-- npx -y @deepfirstsearch/agent-pay-mcp /absolute/path/config.jsonOpenClaw
npm install -g @deepfirstsearch/agent-pay-mcp # or use "command":"npx","args":["-y","@deepfirstsearch/agent-pay-mcp","/absolute/path/config.json"]
openclaw mcp set agent-pay '{"command":"agent-pay-mcp","args":["/absolute/path/config.json"],"env":{"AGENT_PAY_AGENT_KEY":"${AGENT_PAY_AGENT_KEY}","AGENT_PAY_BURNER_SEED":"${AGENT_PAY_BURNER_SEED}"}}'
openclaw mcp probe agent-pay # - agent-pay: 3 toolsFull walkthrough: OpenClaw guide.
Claude Desktop / Cursor
claude_desktop_config.json (Claude Desktop) or .cursor/mcp.json (Cursor):
{
"mcpServers": {
"agent-pay": {
"command": "npx",
"args": ["-y", "@deepfirstsearch/agent-pay-mcp", "/absolute/path/config.json"],
"env": { "AGENT_PAY_AGENT_KEY": "0x…", "AGENT_PAY_BURNER_SEED": "0x…" }
}
}
}From source instead of npm: cd integrations/mcp && npm ci && npm run build, then use node /path/to/integrations/mcp/dist/index.js as the command.
Quick start with verified sellers (Base mainnet)
The verified preset ships 35 x402 sellers whose payee and price were confirmed with a real payment, each with a guide. They are re-checked live, without paying, by scripts/verify-preset.ts, and the list is in src/presets.ts. You pick the ones you use and set your total budget:
{
"network": "eip155:8453",
"preset": "verified",
"presetOnly": ["https://netintel.dev"],
"periodBudget": { "amount": "1.00", "hours": 24 }
}Save it as preset.json and start the server with AGENT_PAY_BURNER_SEED=0x$(openssl rand -hex 32) npx -y @deepfirstsearch/agent-pay-mcp ./preset.json. On start it prints the payer address for each seller. Send that address a little USDC on Base (0.10 covers dozens of calls). Then ask your agent to check_price and paid_fetch a seller's URL, for example https://netintel.dev/web/extract?url=https://www.sitemaps.org/protocol.html. Keep the seed: the same seed gives the same payer addresses.
- Your own entries win: if you list a merchant with the same origin under
merchants, the preset's entry is ignored. - Choosing sellers:
presetOnlylimits the preset to those origins;presetExcludeleaves some out;presetMaxSpend(default"0.25") caps each seller per plan window. - Budget:
periodBudgetis required with a preset. It's your total across all of them. - Vaults: the preset works with payers you fund yourself for now. With a vault, list your merchants with their
intentIdinstead.
Try it offline (1 minute)
No keys to fund, no chain: run a local mock merchant from the SDK and point the server at it.
npm install -g @deepfirstsearch/agent-pay-mcp # or skip this and use `npx -y @deepfirstsearch/agent-pay-mcp` below
mkdir mock && cd mock && npm init -y >/dev/null && npm install @deepfirstsearch/agent-pay
node --input-type=module -e '
import { startMockServer } from "@deepfirstsearch/agent-pay/testing";
const m = await startMockServer({ "/data": { price: 10000n, payTo: "0x1111111111111111111111111111111111111111", body: "{\"ok\":true}" } });
console.log("origin:", m.url);'Leave it running. In offline.json, use the printed origin: { "network": "eip155:84532", "merchants": [{ "origin": "<printed origin>", "payTo": "0x1111111111111111111111111111111111111111", "price": "0.01", "maxPerTx": "0.05", "maxSpend": "0.50" }] }. Then start the server with AGENT_PAY_BURNER_SEED=0x$(openssl rand -hex 32) agent-pay-mcp ./offline.json (without a global install: AGENT_PAY_BURNER_SEED=0x$(openssl rand -hex 32) npx -y @deepfirstsearch/agent-pay-mcp ./offline.json), or add it to your MCP client, and call paid_fetch with <printed origin>/data. The mock verifies signatures locally, so the payer needs no funds.
See it work in 5 minutes (Base Sepolia)
sdk/examples/demo-merchant.ts is a tiny x402 API on Base Sepolia with an honest route (/premium, 0.01 USDC) and a hostile one (/malicious: its 402 asks for 5 USDC to an attacker address and its body carries a prompt injection).
cd sdk && MERCHANT=0xYourMerchantAddress npx tsx examples/demo-merchant.tsPoint the config's merchant at http://127.0.0.1:4021 with that payTo, then ask your agent to fetch /premium and /malicious. Expected: the first is paid and settled on-chain; the second is refused before anything is signed ("payTo … is not the merchant's registered address").
Config reference
| Field | Meaning |
|---|---|
| network | eip155:84532 (Base Sepolia) or eip155:8453 (Base) |
| rpcUrl | Optional RPC URL for that network (vault top-ups and balance reads); defaults to viem's public RPC |
| vault, tranche | Agent Safe vault that tops up each merchant's payer, and the top-up size |
| merchants[].origin, payTo | Who may be paid, and the only address the payment can go to |
| merchants[].intentId | The owner-signed budget (intent) for this merchant, as printed by owner budget; required for every merchant when vault is set |
| merchants[].label | Optional display name (up to 80 characters) shown by list_merchants |
| merchants[].price, tolerancePct | Expected price per call; anything above price × (1 + tolerance) is refused |
| merchants[].maxPerTx, maxSpend | Hard cap per call, and per merchant per plan window |
| merchants[].maxTimeoutSeconds | Longest authorization this merchant may ask for, 10 to 86400 s (default 300 s). Only that merchant gets the longer window. |
| merchants[].x402Versions | x402 versions this merchant may be paid with, default [2]. Set [1, 2] for merchants that still speak x402 v1 (e.g. Heurist Mesh). |
| confirmOnChain | Default true. When a merchant's receipt is missing or unreadable, USDC is asked on-chain (through rpcUrl) whether the payment executed, instead of resending. Needed for merchants with non-standard receipts (CoinMarketCap, JustaName, Glim). Only used on that failure path. |
| planWindowHours | The plan is sealed from this file at start and renewed from it every window |
| periodBudget | Total across merchants per period: { "amount": "1.00", "hours": 24 } |
| approvalAbove | Payments above this are refused (this server has no approval channel the model can't reach) |
| sessionHasSensitiveData | If the agent can also read private data, every payment needs a human (Rule of Two), so all are refused |
| auditLog | Hash-chained JSONL log of every decision |
| maxResponseChars | Longest response body returned to the model, in characters (default 20000, max 1000000); longer bodies are truncated and say so |
| allowPrivateNetwork | Default false. Set true to let paid_fetch reach http:// URLs and loopback, private, link-local or CGNAT hosts that are not merchants in this file |
Unknown keys are rejected, so a typo (approvalabove) stops the server instead of silently dropping a limit.
Since 0.2.0 every tested merchant works through MCP: set x402Versions: [1, 2] for v1 merchants (Heurist Mesh), and leave confirmOnChain on for merchants with non-standard receipts (CoinMarketCap, JustaName, Glim). Without a vault, the server prints each merchant's payer address on start, so you know which address to fund.
A refused payment comes back as an error result, Payment refused by policy: …. From 0.1.8, once the bundled @deepfirstsearch/agent-pay is 0.8.0 or later, the text carries the refusal's stable code and what to do about it, e.g. Payment refused by policy [price_changed → ask the owner]: … (codes listed in the SDK README, "Refusal codes"). Blocks carry codes too (Payment blocked [settlement_pending → …]: …; also settled_not_delivered, rate_limited, kill_switch), with the extra action resend_same: call paid_fetch on the same URL again, and the SDK resends the same signed authorization without signing a new one.
paid_fetch fetches any public https URL the model asks for; only payments are restricted to the merchants in your config. A non-402 response from an unregistered origin is returned as data (fenced as untrusted), so treat it like any fetch tool you give a model. Hosts that are or resolve to loopback, private, link-local or CGNAT addresses, localhost names and plain http are refused unless they are a merchant in your config or you set allowPrivateNetwork; redirects are returned, not followed. Errors other than policy refusals (for example an RPC failure, whose message can contain your rpcUrl) reach the model as a fixed text; the details go to the server's stderr.
Develop
npm ci && npm run typecheck && npm test # tests drive the server through an MCP client against a mock x402 merchant