branch:
README.md
3135 bytesRaw
# x402 MCP — Paid Tools
Paid MCP tools using the [x402 payment protocol](https://x402.org). 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.
## What it demonstrates
- **`withX402(server, config)`** — wrapping an MCP server to add `paidTool()` for tools that cost money
- **`withX402Client(client, config)`** — wrapping an MCP client to handle payment flows automatically
- **`paidTool()`** — registering a tool with a USD price
- **`@callable`** — the client agent exposes `callTool` and `resolvePayment` as callable methods
- **`useAgent` + `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
## Running
Copy the environment file and fill in your keys:
```sh
cp .env.example .env
```
You 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:
```sh
npm install
npm start
```
Try the free **echo** tool and the paid **square** tool ($0.01) — the payment modal appears when calling paid tools.
## How it works
### Server: creating paid tools
```typescript
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) }]
})
);
```
### Client agent: calling paid tools
```typescript
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);
}
}
```
### React frontend
```typescript
const agent = useAgent({ agent: "pay-agent", name: sessionId });
const result = await agent.call("callTool", ["square", { number: 5 }]);
```
### x402 MCP transport spec
This follows the [x402 MCP transport specification](https://github.com/coinbase/x402/blob/main/specs/transports/mcp.md):
1. **402 error** — server returns JSON-RPC error with `code: 402` and `PaymentRequirementsResponse`
2. **Payment payload** — client retries with payment in `_meta["x402/payment"]`
3. **Settlement** — server confirms in `_meta["x402/payment-response"]`
## Related examples
- [`mcp`](../mcp/) — stateful MCP server (no payments)
- [`mcp-client`](../mcp-client/) — connecting to MCP servers as a client