|
| 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. |
0 commit comments