Skip to content

Commit 470181b

Browse files
committed
feat: use custom meta for price discovery.
1 parent 978a358 commit 470181b

File tree

2 files changed

+111
-7
lines changed

2 files changed

+111
-7
lines changed

examples/x402-mcp/README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# x402 MCP Example
2+
3+
This example demonstrates how to create paid MCP tools using the [x402 payment protocol](https://x402.org) with Cloudflare Agents.
4+
5+
## Overview
6+
7+
The example includes:
8+
9+
- **PayMCP**: An MCP server that exposes paid and free tools
10+
- **PayAgent**: A client agent that calls these tools and handles payment confirmation flows
11+
12+
## x402 MCP Integration
13+
14+
This implementation follows the [x402 MCP transport specification](https://github.com/coinbase/x402/blob/main/specs/transports/mcp.md#payment-payload-transmission), which defines:
15+
16+
1. **Payment Required Signaling**: Server returns JSON-RPC error with `code: 402` and `PaymentRequirementsResponse`
17+
2. **Payment Payload Transmission**: Client sends payment in `_meta["x402/payment"]`
18+
3. **Settlement Response**: Server confirms payment in `_meta["x402/payment-response"]`
19+
20+
### Price Discovery Extension
21+
22+
In addition to the core x402 MCP spec, this implementation includes a **Agents extension** for price discovery:
23+
24+
```typescript
25+
_meta: {
26+
"agents-x402/paymentRequired": true, // Indicates tool requires payment
27+
"agents-x402/priceUSD": 0.01 // Pre-advertises price in USD
28+
}
29+
```
30+
31+
**Note**: The `agents-x402/` namespace is used (not `x402/`) because price pre-advertising is an extension beyond the official x402 MCP specification to allow for a nice user experience. The core spec only defines the reactive payment flow (call → 402 error → retry with payment).
32+
33+
## Usage
34+
35+
### Server Side: Creating Paid Tools
36+
37+
```typescript
38+
import { withX402 } from "agents/x402";
39+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
40+
41+
const server = withX402(new McpServer({ name: "PayMCP", version: "1.0.0" }), {
42+
network: "base-sepolia",
43+
recipient: "0x...",
44+
facilitator: { url: "https://x402.org/facilitator" }
45+
});
46+
47+
// Create a paid tool
48+
server.paidTool(
49+
"square",
50+
"Squares a number",
51+
0.01, // Price in USD
52+
{ number: z.number() },
53+
{}, // MCP annotations (readOnlyHint, etc.)
54+
async ({ number }) => {
55+
return { content: [{ type: "text", text: String(number ** 2) }] };
56+
}
57+
);
58+
```
59+
60+
### Client Side: Calling Paid Tools
61+
62+
```typescript
63+
import { withX402Client } from "agents/x402";
64+
import { privateKeyToAccount } from "viem/accounts";
65+
66+
const account = privateKeyToAccount(env.PRIVATE_KEY);
67+
68+
const x402Client = withX402Client(mcpClient, {
69+
network: "base-sepolia",
70+
account
71+
});
72+
73+
// Call tool with payment confirmation callback
74+
const result = await x402Client.callTool(
75+
async (requirements) => {
76+
// Show payment prompt to user
77+
return await userConfirmsPayment(requirements);
78+
},
79+
{
80+
name: "square",
81+
arguments: { number: 5 }
82+
}
83+
);
84+
```
85+
86+
## Environment Variables
87+
88+
```bash
89+
# Server: Address to receive payments
90+
MCP_ADDRESS=0x...
91+
92+
# Client: Private key for signing payments
93+
CLIENT_TEST_PK=0x...
94+
```
95+
96+
## Running the Example
97+
98+
```bash
99+
npm install
100+
npx wrangler dev
101+
```
102+
103+
Then open http://localhost:8787 in your browser.

packages/agents/src/mcp/x402.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ export function withX402<T extends McpServer>(
7878
{
7979
description,
8080
inputSchema: paramsSchema,
81-
annotations: {
82-
...annotations,
83-
paymentHint: true,
84-
paymentPriceUSD: priceUSD
81+
annotations,
82+
_meta: {
83+
"agents-x402/paymentRequired": true,
84+
"agents-x402/priceUSD": priceUSD
8585
}
8686
},
8787
(async (args, extra) => {
@@ -265,9 +265,10 @@ export function withX402Client<T extends MCPClient>(
265265
const toolsRes = await _listTools(params, options);
266266
toolsRes.tools = toolsRes.tools.map((tool) => {
267267
let description = tool.description;
268-
if (tool.annotations?.paymentHint) {
269-
const cost = tool.annotations?.paymentPriceUSD
270-
? `$${tool.annotations?.paymentPriceUSD}`
268+
// Check _meta for payment information (agents-x402/ is our extension for pre-advertising prices)
269+
if (tool._meta?.["agents-x402/paymentRequired"]) {
270+
const cost = tool._meta?.["agents-x402/priceUSD"]
271+
? `$${tool._meta?.["agents-x402/priceUSD"]}`
271272
: "an unknown amount";
272273
description += ` (This is a paid tool, you will be charged ${cost} for its execution)`;
273274
}

0 commit comments

Comments
 (0)