Skip to content

Commit 17810f3

Browse files
committed
feat(metamask-agent-wallet): add x402 buyer payment support
Agents that fetch APIs and resources increasingly hit HTTP 402 (x402) paywalls and must pay per request, but the CLI has no native x402 support, so an agent improvises the protocol and the EIP-3009 signing, which is error-prone and skips payment safety checks. Guide the agent to detect a 402, select an offered exact-scheme option from an asset allowlist, sign an EIP-3009 transferWithAuthorization with mm wallet sign-typed-data, retry with the payment header, and verify settlement. Handle protocol v1 (X-PAYMENT) and v2 (PAYMENT-SIGNATURE). Confirm before signing; no autonomous auto-pay.
1 parent 488d6c2 commit 17810f3

3 files changed

Lines changed: 276 additions & 2 deletions

File tree

skills/metamask-agent-wallet/SKILL.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
name: metamask-agent-wallet
3-
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. Single entry point for all mm CLI operations.
3+
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.
44
license: MIT
55
metadata:
66
author: metamask
7-
version: "3.0.0"
7+
version: "3.1.0"
88
cliVersion: "2.0.0"
99
---
1010

@@ -105,6 +105,7 @@ Match the user's intent to a command and reference file, then read the reference
105105
| Execute a token swap or bridge | `mm swap execute` | [swap.md](references/swap.md) |
106106
| Check swap or bridge status | `mm swap status` | [swap.md](references/swap.md) |
107107
| Bridge tokens to another chain | `mm swap execute` | [swap.md](references/swap.md) |
108+
| Pay an HTTP `402` / x402 paywalled request | `mm wallet sign-typed-data` (+ `curl`) | [x402.md](references/x402.md) |
108109

109110
## Workflows
110111

@@ -134,6 +135,7 @@ CLI behavior lives in `references/`. Repeatable patterns live in `workflows/`. L
134135
| Toggle Aave V3 collateral | [aave-collateral.md](workflows/aave-collateral.md) |
135136
| Check Aave V3 positions and health factor | [aave-positions.md](workflows/aave-positions.md) |
136137
| Discover Aave V3 tokens, rates, and liquidity | [aave-markets.md](workflows/aave-markets.md) |
138+
| Pay an HTTP `402` (x402) paywalled request | [x402-pay.md](workflows/x402-pay.md) |
137139

138140
## Global Flags
139141

@@ -209,6 +211,10 @@ Before constructing any command, validate all user-provided values:
209211
| `--from-chain`, `--to-chain` | Must be a positive integer EVM chain ID |
210212
| `--to-address` | Must match `^0x[0-9a-fA-F]{40}$`. Only valid for cross-chain swaps (`--to-chain` differs from `--from-chain`); rejected for same-chain swaps |
211213
| `--password` | Must be a non-empty string. Never log, display, or store the value. |
214+
| x402 `asset` | Must be an allowlisted EIP-3009 stablecoin contract (mUSD, USDC) on a chain returned by `mm chains list`. Reject any other contract. |
215+
| x402 `payTo` / authorization `to` | Must match `^0x[0-9a-fA-F]{40}$` and equal the recipient in the `402` |
216+
| x402 `value` | Atomic-unit integer that exactly equals the offered amount (the `exact` scheme is not a maximum) |
217+
| x402 resource URL | Must be `https://`. Reject a `402` reached via an unexpected cross-host redirect |
212218

213219
Do not pass unvalidated user input into any command.
214220

@@ -221,6 +227,7 @@ Do not pass unvalidated user input into any command.
221227
| Message signing | Always show exact message and chain before signing |
222228
| Typed-data signing | Always show domain, primary type, chain, verifying contract, and message summary before signing |
223229
| Swaps / bridges | Always confirm from/to tokens, amount, source/destination chain, slippage, quoted output, and recipient address (if `--to-address` is set) before executing |
230+
| 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. |
224231
| Perps trading | Always confirm symbol, side, size, leverage, venue, order type, and limit price if present before executing |
225232
| Perps deposit/withdraw | Always confirm amount, asset, venue, network, and destination where applicable before executing |
226233
| Predict trading | Always confirm token ID, side, size, price, order type, market, and outcome before executing |
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# x402 Payments (buyer / payer)
2+
3+
Use this when an HTTP request returns **`402 Payment Required`** following the
4+
[x402 protocol](https://docs.x402.org), or when the user asks the agent to pay for / fetch a
5+
paywalled API, endpoint, file, or resource over HTTP.
6+
7+
The agent performs the HTTP itself (with `curl`). The wallet's only role is to **sign** an
8+
EIP-712 authorization, and the private key never leaves `mm`. There is **no `mm x402` command**;
9+
this recipe composes `mm wallet sign-typed-data` with `curl`.
10+
11+
> Money-movement: a signature here authorizes a real token debit. Follow the confirmation and
12+
> validation rules below before signing. See [transfer.md](transfer.md) for the wallet's
13+
> general confirmation norms and `SKILL.md` › Safety Rules.
14+
15+
## The exact-scheme flow (EVM)
16+
17+
1. Request the resource with the status code visible.
18+
2. On `402`, read the **payment requirements** (one or more options the server accepts).
19+
3. Select an offered option whose scheme is `exact` and whose asset is allowlisted (below).
20+
4. Validate + confirm the payment with the user.
21+
5. Build an EIP-3009 `TransferWithAuthorization` as EIP-712 typed data and sign it. This is a
22+
**gasless, off-chain authorization** (no on-chain tx from the payer) permitting a pull of
23+
exactly `value` of `asset` to `payTo`.
24+
6. Base64-encode the payment payload and **retry** with the payment header.
25+
7. The server's facilitator settles on-chain and returns the resource + a settlement header.
26+
27+
## Protocol versions (detect, don't assume)
28+
29+
Two shapes exist in the wild. Detect the version from the `402` and echo the server's own
30+
`scheme`/`network` identifiers back verbatim.
31+
32+
| | **v1** | **v2** |
33+
| --- | --- | --- |
34+
| Requirements delivered in | `402` JSON **body**, `accepts[]` | `PAYMENT-REQUIRED` response **header** (base64 JSON) |
35+
| Requirement field names | `maxAmountRequired`, `payTo`, `resource`, `asset`, `extra` | `amount`, `payTo`, `asset`, `extra` |
36+
| Retry header (client → server) | `X-PAYMENT` | `PAYMENT-SIGNATURE` |
37+
| Settlement header (server → client) | `X-PAYMENT-RESPONSE` | `PAYMENT-RESPONSE` |
38+
| `network` identifier | name string (`base`, `base-sepolia`) | CAIP-2 (`eip155:8453`) |
39+
| Payload `x402Version` | `1` | `2` |
40+
41+
### v1 `402` body example
42+
```json
43+
{
44+
"x402Version": 1,
45+
"error": "X-PAYMENT header is required",
46+
"accepts": [{
47+
"scheme": "exact",
48+
"network": "base",
49+
"maxAmountRequired": "10000",
50+
"resource": "https://api.example.com/premium",
51+
"payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
52+
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
53+
"maxTimeoutSeconds": 60,
54+
"extra": { "name": "USD Coin", "version": "2" }
55+
}]
56+
}
57+
```
58+
59+
## Requirement fields → what they mean
60+
61+
| Field | Meaning / use |
62+
| --- | --- |
63+
| `scheme` | Must be `exact` for this recipe. Reject other schemes (Permit2/EIP-2612 paths are out of scope). |
64+
| `network` | Target chain. Map to a chain ID via `mm chains list` (see [chain.md](chain.md)); do not guess. |
65+
| `maxAmountRequired` (v1) / `amount` (v2) | The **exact** atomic-unit amount to authorize. For the `exact` scheme this is the precise `value`, not a maximum. |
66+
| `payTo` | Recipient address. Becomes `to` in the authorization. |
67+
| `asset` | Token contract. Becomes the EIP-712 `verifyingContract`. Must be an allowlisted stablecoin (below). |
68+
| `extra.name` / `extra.version` | The token's EIP-712 domain `name` and `version`. |
69+
| `maxTimeoutSeconds` | Settlement window. Bound `validBefore` to it. |
70+
71+
## EIP-3009 typed data (what `mm` signs)
72+
73+
The `exact` scheme uses **`TransferWithAuthorization`** (not `ReceiveWithAuthorization`).
74+
75+
```
76+
EIP712Domain(string name, string version, uint256 chainId, address verifyingContract)
77+
TransferWithAuthorization(address from, address to, uint256 value,
78+
uint256 validAfter, uint256 validBefore, bytes32 nonce)
79+
```
80+
81+
Construct the `--payload` JSON:
82+
- `domain.name`, `domain.version` ← the option's `extra` (USDC/mUSD use `"2"`). No `salt`.
83+
- `domain.chainId` ← chain ID for the option's `network`.
84+
- `domain.verifyingContract` ← the option's `asset`.
85+
- `message.from``mm wallet address` (see [wallet.md](wallet.md)).
86+
- `message.to``payTo`; `message.value` ← the exact amount.
87+
- `message.validAfter``"0"`; `message.validBefore``now + min(maxTimeoutSeconds, 60)`
88+
(short window; never sign a long-lived authorization). Get `now` with `date +%s`.
89+
- `message.nonce` ← fresh random 32 bytes: `openssl rand -hex 32`, `0x`-prefixed.
90+
- `primaryType` = `TransferWithAuthorization`. Include `EIP712Domain` in `types` (`mm` accepts it).
91+
92+
### Sign
93+
```bash
94+
mm wallet sign-typed-data \
95+
--chain-id <id> --wait --json \
96+
--intent "x402: <amount> <asset-symbol> to <payTo> for <resource>" \
97+
--payload '<typed-data-json>'
98+
```
99+
- `--intent`: human-readable summary forwarded to the wallet. Always set it for x402.
100+
- `--wait`: required in server-wallet mode (signing is async otherwise; see below).
101+
- Signature is returned at `.data.signature` (`{"ok":true,"data":{"status":"SIGNED","signature":"0x…"}}`).
102+
103+
**Critical:** the `nonce` and `validBefore` in the signed `message` and in the payload's
104+
`authorization` (below) must be **byte-identical**. Build them once, reuse the same strings.
105+
106+
## Assemble the payment payload and retry
107+
108+
```json
109+
{
110+
"x402Version": 1,
111+
"scheme": "exact",
112+
"network": "base",
113+
"payload": {
114+
"signature": "0x…",
115+
"authorization": {
116+
"from": "0x…", "to": "0x209693…287C", "value": "10000",
117+
"validAfter": "0", "validBefore": "<computed>",
118+
"nonce": "0x<same-nonce>"
119+
}
120+
}
121+
}
122+
```
123+
Echo `scheme`/`network`/`x402Version` from the selected option. Base64-encode the minified JSON
124+
(`base64 | tr -d '\n'`) and retry:
125+
126+
```bash
127+
# v1
128+
curl -sS -i https://api.example.com/premium -H "X-PAYMENT: <base64>"
129+
# v2: header name is PAYMENT-SIGNATURE instead
130+
```
131+
Preserve the original method/body/headers on the retry.
132+
133+
On `200`: parse the settlement header (`X-PAYMENT-RESPONSE` / `PAYMENT-RESPONSE`, base64) for the
134+
on-chain tx, report it, and return the resource. **If the retry is still `402` or any 4xx/5xx,
135+
surface it verbatim and do NOT pay again** (one payment attempt per resource, no retry loop).
136+
137+
## Asset allowlist
138+
139+
Only authorize known EIP-3009 stablecoins (below) on a chain from `mm chains list`. Do not rank
140+
options by token identity. If more than one offered option qualifies, present the choices and let
141+
the user pick. You confirm before signing regardless.
142+
143+
| Asset | Decimals | Chain (id) | Contract |
144+
| --- | --- | --- | --- |
145+
| mUSD | 6 | Ethereum (1) | `0xaca92e438df0b2401ff60da7e4337b687a2435da` |
146+
| mUSD | 6 | Linea (59144) | confirm via `mm token` discovery ([market-data.md](market-data.md)) |
147+
| USDC | 6 | Ethereum (1) | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` |
148+
| USDC | 6 | Base (8453) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
149+
| USDC | 6 | Base Sepolia (84532) | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
150+
151+
The server chooses which assets it accepts; the client can only pick among offered options and
152+
**cannot substitute** an asset the server did not list. Convert atomic `value` → human amount
153+
with the token's decimals (all above use 6) before display.
154+
155+
## Validation (the checks `mm` cannot do)
156+
157+
`mm` signs an opaque typed-data blob; it does not understand x402 semantics. Enforce here:
158+
159+
- **Scheme** is `exact`; reject anything else.
160+
- **Asset** (`verifyingContract`) is an allowlisted stablecoin on a chain from `mm chains list`.
161+
- **Amount integrity:** signed `value` == the option's amount; show the decimals-correct USD value.
162+
- **Recipient:** `to` == `payTo`.
163+
- **Authorization scope:** exactly `value` (an `exact` transfer), never an open-ended allowance.
164+
- **Window/nonce:** short `validBefore`; fresh random nonce; identical across message and payload.
165+
- **HTTPS only**; validate the resource URL; **do not** honor a `402` reached via an unexpected
166+
cross-host redirect (you could pay the wrong host).
167+
- **Settlement:** confirm the resource was actually returned; a missing/failed settlement is not
168+
success.
169+
- **Audit:** report asset, USD amount, `payTo`, resource URL, and the settlement tx after paying.
170+
171+
## Wallet modes
172+
173+
- **Server-wallet (current default):** signing is async; pass `--wait` to block and get the
174+
signature inline (guard mode permits EIP-712 signing). The x402 window is short, so if approval
175+
is required and slow, the authorization can expire; retry with a fresh nonce/timestamps.
176+
- **BYOK:** signing returns immediately; set `MM_PASSWORD` for an encrypted mnemonic so signing
177+
is non-interactive.
178+
179+
## Confirmation
180+
181+
Before signing, present asset, decimals-correct USD amount, network, `payTo`, resource URL, and
182+
the decoded `transferWithAuthorization` intent; proceed only on explicit user approval. Autonomous
183+
auto-pay (paying without per-payment confirmation) is **not** supported by this recipe.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# x402 pay workflow (buyer)
2+
3+
Use this when an HTTP request returns `402 Payment Required` (x402), or the user asks to
4+
fetch/pay for a paywalled API, endpoint, file, or resource over HTTP.
5+
6+
Command syntax and protocol detail live in `references/x402.md`. The agent runs the HTTP;
7+
`mm` only signs.
8+
9+
## Flow
10+
11+
1. Fetch with the status code visible.
12+
2. Detect version and parse the payment requirements.
13+
3. Select a payment option.
14+
4. Validate, then confirm with the user.
15+
5. Sign the authorization.
16+
6. Retry with the payment header.
17+
7. Verify settlement and return the resource.
18+
19+
Don't skip the confirmation step. A signature authorizes a real token debit.
20+
21+
## Fetch
22+
23+
```bash
24+
curl -sS -i <url> # -i so a 402 status is visible, not swallowed
25+
```
26+
Not `402` → return the response. Done.
27+
28+
## Parse
29+
30+
- v1: requirements are in the `402` JSON **body** (`accepts[]`).
31+
- v2: requirements are in the `PAYMENT-REQUIRED` response **header** (base64 JSON).
32+
33+
Extract each option's `scheme`, `network`, amount, `payTo`, `asset`, and `extra`. See the version
34+
table in `references/x402.md`.
35+
36+
## Select
37+
38+
Select an offered option whose scheme is `exact` and whose asset is an allowlisted stablecoin on
39+
a supported chain. If more than one qualifies, present the choices and let the user pick. Reject
40+
non-`exact` schemes and non-allowlisted assets/chains. The server dictates which assets are on
41+
offer; you cannot substitute one it did not list.
42+
43+
## Validate + confirm
44+
45+
Run the checks in `references/x402.md` › Validation (scheme, asset allowlist, amount integrity,
46+
recipient, authorization scope, short window, https-only, no cross-host redirect). Then show the
47+
user: asset, decimals-correct USD amount, network, `payTo`, resource URL, and the decoded
48+
`transferWithAuthorization`. Proceed only on explicit approval.
49+
50+
## Sign
51+
52+
Build the EIP-3009 typed data (see reference) and:
53+
54+
```bash
55+
mm wallet sign-typed-data --chain-id <id> --wait --json \
56+
--intent "x402: <amount> <symbol> to <payTo> for <resource>" \
57+
--payload '<typed-data-json>'
58+
```
59+
Signature at `.data.signature`. Reuse the **same** `nonce` and `validBefore` in the payload.
60+
61+
## Retry
62+
63+
Assemble the payment payload, base64-encode it, and resend with the version's header
64+
(`X-PAYMENT` for v1, `PAYMENT-SIGNATURE` for v2), preserving the original method/body.
65+
66+
```bash
67+
curl -sS -i <url> -H "X-PAYMENT: <base64>"
68+
```
69+
70+
## Verify
71+
72+
On `200`: parse the settlement header (`X-PAYMENT-RESPONSE` / `PAYMENT-RESPONSE`) for the tx,
73+
report asset/amount/payTo/resource/tx, and return the resource.
74+
75+
## Edge cases
76+
77+
- Still `402` or any 4xx/5xx after paying: surface verbatim. **Do not pay again**, one attempt
78+
per resource.
79+
- Unknown/unparseable `402` or version: stop, surface raw response, do not sign.
80+
- No allowlisted/satisfiable option: stop and explain what was offered.
81+
- Over-broad or mismatched authorization request: refuse and explain.
82+
- Missing chain mapping for `network`: use `mm chains list`, never guess.
83+
- Authorization expired (slow approval / short window): re-sign with a fresh nonce and timestamps.
84+
- Encrypted BYOK mnemonic: set `MM_PASSWORD` so signing is non-interactive.

0 commit comments

Comments
 (0)