-
Notifications
You must be signed in to change notification settings - Fork 7
feat(x402): add the MCP transport to the buyer skill #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| __pycache__/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| --- | ||
| name: metamask-agent-wallet | ||
| description: Use when the user asks anything about blockchain wallets, transactions, signing, token transfers, supported chains, wallet balances, perpetual futures trading, prediction markets, token swaps, cross-chain bridges, market data, token discovery, decoding EVM calldata, Aave V3 lending and borrowing, or authentication via the MetaMask Agentic CLI; also when an HTTP request returns 402 Payment Required (x402) or the agent needs to pay for a paywalled API, endpoint, file, or resource over HTTP. Single entry point for all mm CLI operations. | ||
| description: Use when the user asks anything about blockchain wallets, transactions, signing, token transfers, supported chains, wallet balances, perpetual futures trading, prediction markets, token swaps, cross-chain bridges, market data, token discovery, decoding EVM calldata, Aave V3 lending and borrowing, or authentication via the MetaMask Agentic CLI; also when an HTTP request returns 402 Payment Required (x402), when an MCP tool call returns an x402 payment-required result, or the agent needs to pay for a paywalled API, endpoint, file, tool, or resource. Single entry point for all mm CLI operations. | ||
| license: MIT | ||
| metadata: | ||
| author: metamask | ||
|
|
@@ -105,6 +105,7 @@ Match the user's intent to a command and reference file, then read the reference | |
| | Check swap or bridge status | `mm swap status` | [swap.md](references/swap.md) | | ||
| | Bridge tokens to another chain | `mm swap execute` | [swap.md](references/swap.md) | | ||
| | Pay an HTTP `402` / x402 paywalled request | `python3 scripts/x402_pay.py` | [x402.md](references/x402.md) | | ||
| | Pay an x402 challenge from a paid MCP tool | `python3 scripts/x402_pay.py` | [x402.md](references/x402.md) | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pay an x402-gated MCP tool call |
||
|
|
||
| ## Workflows | ||
|
|
||
|
|
@@ -135,6 +136,7 @@ CLI behavior lives in `references/`. Repeatable patterns live in `workflows/`. L | |
| | Check Aave V3 positions and health factor | [aave-positions.md](workflows/aave-positions.md) | | ||
| | Discover Aave V3 tokens, rates, and liquidity | [aave-markets.md](workflows/aave-markets.md) | | ||
| | Pay an HTTP `402` (x402) paywalled request | [x402-pay.md](workflows/x402-pay.md) | | ||
| | Pay an x402-gated MCP tool call | [x402-pay.md](workflows/x402-pay.md) | | ||
|
|
||
| ## Global Flags | ||
|
|
||
|
|
@@ -224,6 +226,7 @@ Before constructing any command, validate all user-provided values: | |
| | x402 `payTo` / authorization `to` | Must match `^0x[0-9a-fA-F]{40}$` and equal the recipient in the `402` | | ||
| | x402 `value` | Atomic-unit integer that exactly equals the offered amount (the `exact` scheme is not a maximum) | | ||
| | x402 resource URL | Must be `https://`. Reject a `402` reached via an unexpected cross-host redirect | | ||
| | x402 MCP challenge | x402 v2 only. The signed `payment` object goes, as-is, in `_meta["x402/payment"]` of the retried tool call; a repeated `isError` result is a new challenge, never re-sign it without new user approval | | ||
|
|
||
| Do not pass unvalidated user input into any command. | ||
|
|
||
|
|
@@ -236,7 +239,7 @@ Do not pass unvalidated user input into any command. | |
| | Message signing | Always show exact message and chain before signing | | ||
| | Typed-data signing | Always show domain, primary type, chain, verifying contract, and message summary before signing | | ||
| | Swaps / bridges | Always confirm from/to tokens, amount, source/destination chain, slippage, quoted output, recipient address (if `--to-address` is set), and the destination gas top-up (if `--refuel` is set) before executing | | ||
| | x402 payments | Always confirm asset, decimals-correct amount, network, `payTo`, and resource URL before signing the authorization. One payment attempt per resource, never auto-retry a payment. Autonomous auto-pay is not supported. | | ||
| | x402 payments | Always confirm asset, decimals-correct amount, network, `payTo`, and resource (URL or MCP tool) before signing the authorization — over HTTP with `pay --confirm`, over MCP with `mcp-sign --confirm`. One payment attempt per resource, never auto-retry a payment. Autonomous auto-pay is not supported. | | ||
| | Perps trading | Always confirm symbol, side, size, leverage, venue, order type, and limit price if present before executing | | ||
| | Perps deposit/withdraw | Always confirm amount, asset, venue, network, and destination where applicable before executing | | ||
| | Predict trading | Always confirm token ID, side, size, price, order type, market, and outcome before executing | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,16 @@ | ||
| # x402 Payments (buyer / payer) | ||
|
|
||
| Use this when an HTTP request returns **`402 Payment Required`** following the | ||
| [x402 protocol](https://docs.x402.org), or when the user asks to pay for / fetch a paywalled API, | ||
| endpoint, file, or resource over HTTP. | ||
| [x402 protocol](https://docs.x402.org), when a **paid MCP tool** answers with an x402 | ||
| payment-required result (the x402 MCP transport), or when the user asks to pay for / fetch a | ||
| paywalled API, endpoint, file, or resource. | ||
|
|
||
| `scripts/x402_pay.py` performs the payment: it fetches the URL, reads the server's payment | ||
| requirements, signs an EIP-3009 authorization with `mm wallet sign-typed-data`, retries with the | ||
| payment header, and reports the settlement. Signing is delegated to `mm`, so the private key stays | ||
| in the wallet. The script is pure Python standard library (run with `python3`). | ||
| `scripts/x402_pay.py` performs the payment. Over HTTP it fetches the URL, reads the server's | ||
| payment requirements, signs an EIP-3009 authorization with `mm wallet sign-typed-data`, retries | ||
| with the payment header, and reports the settlement. Over MCP it signs the tool's payment | ||
| challenge and prints the payload the caller places in the retried tool call (see | ||
| [MCP transport](#mcp-transport-paid-mcp-tools)). Signing is delegated to `mm`, so the private key | ||
| stays in the wallet. The script is pure Python standard library (run with `python3`). | ||
|
|
||
| ## How x402 works (exact scheme, EVM) | ||
|
|
||
|
|
@@ -30,6 +33,7 @@ per version. | |
|
|
||
| - The `exact` scheme on EVM networks (`eip155:*`) using EIP-3009 | ||
| `transferWithAuthorization`, protocol v1 and v2. | ||
| - Two transports: HTTP (v1 and v2) and MCP (v2 only — the x402 MCP transport is defined for v2). | ||
|
|
||
| ### Not supported (rejected with a clear error) | ||
|
|
||
|
|
@@ -73,14 +77,77 @@ python3 "$SKILL_DIR/scripts/x402_pay.py" inspect https://api.example.com/premium | |
| python3 "$SKILL_DIR/scripts/x402_pay.py" pay https://api.example.com/premium --confirm | ||
| ``` | ||
|
|
||
| ## MCP transport (paid MCP tools) | ||
|
|
||
| x402 v2 also runs over MCP ([spec](https://github.com/coinbase/x402/blob/main/specs/transports-v2/mcp.md)): | ||
| a paid MCP tool answers an unpaid `tools/call` with a tool result carrying `isError: true` and a | ||
| `PaymentRequired` object (in `structuredContent`, mirrored as JSON text in `content[0].text`). | ||
| The client signs the same EIP-3009 authorization as over HTTP and retries the tool call with the | ||
| `PaymentPayload` object in the request's `_meta["x402/payment"]`; the settlement receipt comes | ||
| back in the response's `_meta["x402/payment-response"]`. | ||
|
|
||
| The MCP session (and any OAuth on it) belongs to the caller, so this script does not speak MCP. | ||
| It handles the wallet half — parse the challenge, validate, sign, build the payload — and the | ||
| caller delivers the payload on the retry: | ||
|
|
||
| ```bash | ||
| python3 "$SKILL_DIR/scripts/x402_pay.py" mcp-inspect [--challenge JSON | --challenge-file PATH] | ||
| python3 "$SKILL_DIR/scripts/x402_pay.py" mcp-sign [--challenge JSON | --challenge-file PATH] \ | ||
| --confirm [--asset <contract>] [--network <network>] | ||
| ``` | ||
|
|
||
| The challenge may be passed as the whole JSON-RPC response, the tool result, or the bare | ||
| `PaymentRequired` object — via `--challenge` (inline JSON, or `-` to read stdin) or | ||
| `--challenge-file`. Per the spec, `structuredContent` is preferred and `content[0].text` is the | ||
| fallback; the script also checks `_meta["x402/error"]` (see the Cloudflare note below). Only x402 | ||
| v2 challenges are accepted (the MCP transport is not defined for v1). | ||
|
|
||
| `mcp-inspect` prints the payment options (same fields as HTTP `inspect`) without signing. | ||
| `mcp-sign` requires `--confirm`, signs one eligible option (same selection and validation rules | ||
| as `pay`, including `--asset`/`--network` disambiguation), and prints: | ||
|
|
||
| ```json | ||
| { | ||
| "status": "signed", | ||
| "asset": "USDC", "amount": "0.01", "network": "eip155:84532", "payTo": "0x2096…", | ||
| "metaKey": "x402/payment", | ||
| "payment": { "x402Version": 2, "resource": …, "accepted": …, "payload": … }, | ||
| "paymentBase64": "eyJ4NDAyVmVyc2lvbiI6Mi..." | ||
| } | ||
| ``` | ||
|
|
||
| Take `payment` **as-is** (a raw JSON object, not base64) and set it as `_meta["x402/payment"]` in | ||
| the params of the retried `tools/call` — this is what the spec's text documents. After the retry, | ||
| check the response's `_meta["x402/payment-response"]`: `success: true` plus a `transaction` hash | ||
| means the payment settled. A result with `isError: true` again is a fresh challenge (verification | ||
| or settlement failed); surface it to the user — do not re-sign automatically, since a new | ||
| signature is a new payment authorization. | ||
|
|
||
| ### Cloudflare Agents SDK servers (`agents/x402`) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be under ## Notes Also, use the third person voice instead of first person in the SKILLs. |
||
|
|
||
| Verified live against a real deployment: servers built with Cloudflare's Agents SDK | ||
| (`withX402`/`paidTool`, e.g. `cloudflare/agents examples/x402-mcp`) diverge from the spec text two | ||
| ways. The challenge lives in `_meta["x402/error"]` (there is no `structuredContent` key at all — | ||
| `mcp-inspect`/`mcp-sign` check for this explicitly). And on the retry, the server decodes | ||
| `_meta["x402/payment"]` with `JSON.parse(atob(token))` — a **base64-encoded string**, not the | ||
| inline object the spec shows. Use `paymentBase64` instead of `payment` for these servers. A full | ||
| mainnet round-trip (real Base USDC, Cloudflare's unmodified example, CDP facilitator) confirmed | ||
| both fixes end-to-end. | ||
|
|
||
| Each `mcp-sign` run produces one single-use authorization (random nonce, validity window started | ||
| at signing time). Sign right before the retry; if the retry never happens the authorization | ||
| simply expires unspent. | ||
|
|
||
| ## What the script checks | ||
|
|
||
| It pays an option whose `scheme` is `exact` on a network `mm` supports (resolved from | ||
| `mm chains list`). Before signing it verifies the amount is a positive atomic-unit integer, the | ||
| asset and `payTo` are valid addresses, the EIP-712 domain (`name`/`version`) is present, and the | ||
| URL is `https://` (plain `http` is allowed only on loopback for local testing). It does not follow | ||
| redirects, so a cross-host redirect cannot divert payment. After paying it makes a single retry | ||
| and reports settlement; it never retries a payment. | ||
| and reports settlement; it never retries a payment. The URL and redirect checks belong to the | ||
| HTTP transport; over MCP the connection is the caller's session, and all the other checks apply | ||
| identically before `mcp-sign` signs. | ||
|
|
||
| These are protocol-correctness checks. Which currencies and amounts are allowed is the wallet's | ||
| decision: the asset comes from the server's offer, the user confirms it, and `mm` signs (and is | ||
|
|
@@ -103,8 +170,10 @@ amount; the signed value is the server's atomic amount either way. | |
|
|
||
| ## Confirmation | ||
|
|
||
| Run `inspect` and show the user the asset, amount, network, `payTo`, and resource URL. Run | ||
| `pay ... --confirm` only after the user approves. A signature authorizes a real token debit. | ||
| Run `inspect` (or `mcp-inspect`) and show the user the asset, amount, network, `payTo`, and | ||
| resource. Run `pay ... --confirm` (or `mcp-sign ... --confirm`) only after the user approves. A | ||
| signature authorizes a real token debit — over MCP that holds even though the script never | ||
| touches the network, because the signed payload settles when the caller replays the tool call. | ||
|
Comment on lines
+173
to
+176
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of |
||
|
|
||
| ## Idempotency | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove the gitignore and test case for script. As of now we are not adding the test for scripts.