Paid MCP tools using the x402 payment protocol. Includes both a server (PayMCP) with free and paid tools, and a client agent (PayAgent) that handles the payment confirmation flow — all in one Worker.
withX402(server, config)— wrapping an MCP server to addpaidTool()for tools that cost moneywithX402Client(client, config)— wrapping an MCP client to handle payment flows automaticallypaidTool()— registering a tool with a USD price@callable— the client agent exposescallToolandresolvePaymentas callable methodsuseAgent+agent.call()— the React frontend connects via WebSocket and calls agent methods directly- Payment confirmation flow — paid tools trigger a modal in the UI; the user confirms before the agent signs the payment
Copy the environment file and fill in your keys:
cp .env.example .envYou need:
MCP_ADDRESS— an Ethereum address to receive payments (Base Sepolia testnet)CLIENT_TEST_PK— a private key for signing payments (test key only)
Then:
npm install
npm startTry the free echo tool and the paid square tool ($0.01) — the payment modal appears when calling paid tools.
import { withX402, type X402Config } from "agents/x402";
const server = withX402(new McpServer({ name: "PayMCP", version: "1.0.0" }), {
network: "eip155:84532",
recipient: "0x...",
facilitator: { url: "https://x402.org/facilitator" }
});
server.paidTool(
"square",
"Squares a number",
0.01,
{ number: z.number() },
{},
async ({ number }) => ({
content: [{ type: "text", text: String(number ** 2) }]
})
);import { withX402Client } from "agents/x402";
export class PayAgent extends Agent<Env> {
@callable()
async callTool(toolName: string, args: Record<string, unknown>) {
const res = await this.x402Client.callTool(
this.requestPaymentConfirmation.bind(this),
{ name: toolName, arguments: args }
);
return { text: res.content[0]?.text, isError: res.isError };
}
@callable()
resolvePayment(confirmationId: string, confirmed: boolean) {
this.pendingPayments[confirmationId]?.(confirmed);
}
}const agent = useAgent({ agent: "pay-agent", name: sessionId });
const result = await agent.call("callTool", ["square", { number: 5 }]);This follows the x402 MCP transport specification:
- 402 error — server returns JSON-RPC error with
code: 402andPaymentRequirementsResponse - Payment payload — client retries with payment in
_meta["x402/payment"] - Settlement — server confirms in
_meta["x402/payment-response"]
mcp— stateful MCP server (no payments)mcp-client— connecting to MCP servers as a client