# Paying for API work from an autonomous agent: x402 v2 `exact` + Permit2 on Ethereum mainnet

*A field guide with sources. Research date: 2026-09-25.*

---

## 0. How to read this guide

Every substantive claim carries a tag and a source ID (the sources are listed in §10):

| Tag | Meaning |
|---|---|
| **[F]** | **Fact.** Stated in a primary source (spec text, contract source, reference SDK source) or observed directly on chain. |
| **[I]** | **Inference.** My reasoning from the facts. It is not stated in any source. |
| **[U]** | **Uncertain.** Sources disagree, the behaviour depends on the implementation, or I could not verify it. |
| **[Q]** | **Open question.** I could not answer it from the sources available. |

**Primary sources.** All citations are pinned to fixed commits:

- the x402 specification and reference implementation, `x402-foundation/x402` at commit `4fcf836` (2026-09-25). The v2 spec names this repository as its basis [S2 §1]. An older clone of `coinbase/x402` (`dd927a2`, 2026-04-21) carries the same Permit2 design;
- Uniswap Permit2, `Uniswap/permit2` at commit `cc56ad0`;
- direct JSON-RPC reads against Ethereum mainnet at block 26,056,849 (2026-09-25 20:27 UTC) [S19].

### 0.1 A correction to the brief

The brief describes the flow as "a Permit2 witness signature **and a second EIP-712 signature that binds the payment to the quote**", and lists "a payment hash that does not match" as a failure mode.

- **[F]** The normative x402 v2 `exact`/EVM/Permit2 flow needs **one client signature**: the Permit2 `PermitWitnessTransferFrom` EIP-712 signature. The payload contains only `signature` and `permit2Authorization` [S1 Phase 2]. Nowhere in the core spec, the HTTP transport or the exact-EVM scheme is the client asked to sign a quote, and none of them defines a "payment hash".
- **[F]** The spec does define a second EIP-712 signature, but the **resource server** produces it, not the client. This is the *signed Offer* in the optional `offer-receipt` extension, signed under the domain `{name:"x402 offer", version:"1", chainId:1}` [S13 §4]. It commits the server to `resourceUrl, scheme, network, asset, payTo, amount, validUntil`. It is not checked on chain, and it is not tied cryptographically to the client's Permit2 signature.
- **[F]** Two other things are close to a "payment hash":
  - the `auth-capture` scheme, which is a different scheme from `exact`, derives `paymentInfoHash` and a salt-binding commitment [S15];
  - the repository's e2e test facilitator computes `sha256(JSON.stringify(paymentPayload))` to check that a payload reaching `/settle` was verified within the previous 5 minutes [S23]. This is test code, not protocol.
- **[I]** So the guide below does two things:
  1. It documents the standard flow exactly as specified.
  2. It treats "second signature binding payment to quote" as a **binding layer**. You can build that layer from the signed Offer plus the `accepted` echo plus `payment-identifier`, or a particular facilitator may add a proprietary version of it. §2.3 and §5.4 cover what such a layer protects against and how it fails.

  If your facilitator really does require a client-signed quote binding, it is **vendor-specific**. Get its type string and domain from that vendor. Do not guess them (see §9 Q1).

---

## 1. The components

| Component | On Ethereum mainnet (`eip155:1`) | Evidence |
|---|---|---|
| Canonical Permit2 | `0x000000000022D473030F116dDEE9F6B43aC78BA3` | **[F]** Constant in SDK [S6-const]; bytecode present on mainnet [S19] |
| `x402ExactPermit2Proxy` (the only permitted `spender`) | `0x402085c248EeA27D92E8b30b2C58ed07f9E20001` | **[F]** Spec canonical address [S1 Annex]; bytecode present on mainnet, and `PERMIT2()` returns the canonical Permit2 address [S19] |
| Default mainnet stablecoin in the SDK | USDC `0xA0b8…eB48`, EIP-712 name "USD Coin", version "2" | **[F]** [S20] |
| Client | Your agent. Holds the payer key, signs, and retries the HTTP request | [S2 §3] |
| Resource server | Issues the 402 challenge, calls `/verify` and `/settle` | [S2 §3, §7] |
| Facilitator | Verifies, then broadcasts `proxy.settle(...)` and **pays the gas** | **[F]** "the Facilitator … pays the gas … cannot modify the amount or destination" [S1 Summary] |

**Why Permit2 on mainnet?**

- **[F]** Mainnet USDC supports EIP-3009. The spec says that when `extra.assetTransferMethod` is absent, clients default to `eip3009`, and Permit2 is used only when the server sets `"assetTransferMethod":"permit2"` [S1 Summary; §2 `extra` definitions].
- **[I]** In practice, you will meet Permit2 on mainnet for tokens that lack EIP-3009, or when a server deliberately chooses Permit2. Your client must follow what the challenge says and must not assume a method.

---

## 2. The full message flow

```
Agent (client)                Resource server                 Facilitator                 Ethereum mainnet
     |  (0) optional: discovery / price list (Bazaar)                 |                             |
     |--(1) GET /resource ------------->|                              |                             |
     |<-(2) 402 + PAYMENT-REQUIRED -----|  (price quote = accepts[])   |                             |
     |      [+ optional signed Offer]   |                              |                             |
     |  (3) policy check, allowance/balance pre-check (reads) ------------------------------------->|
     |  (4) sign Permit2 witness (EIP-712, domain Permit2/chainId 1)   |                             |
     |--(5) retry + PAYMENT-SIGNATURE ->|                              |                             |
     |                                  |--(6) POST /verify ---------->| sig, allowance, balance,    |
     |                                  |<-- isValid ------------------| window, amount, simulate -->|
     |                                  |  (7) do the work             |                             |
     |                                  |--(8) POST /settle ---------->|--(9) proxy.settle(...) ---->|
     |                                  |<-- tx hash ------------------|    Permit2 checks + transfer|
     |<-(10) 200 + PAYMENT-RESPONSE ----|  [+ optional signed Receipt] |                             |
```

### 2.1 Price quote and HTTP 402 challenge (steps 1–2)

- **[F]** The server responds `402 Payment Required`. It carries a base64-encoded `PaymentRequired` object in the `PAYMENT-REQUIRED` header [S3].
- **[F]** Each `accepts[]` entry is the quote: `scheme`, `network` (CAIP-2, `eip155:1` for mainnet), `amount` (atomic units, a string), `asset`, `payTo`, `maxTimeoutSeconds` and `extra` [S2 §5.1.2].
  - For Permit2, `extra.assetTransferMethod` MUST be `"permit2"`.
  - `extra.name` and `extra.version` are required only when the token supports EIP-2612 gasless approval [S1 §2].
- **[F]** The reference server SDK sets `maxTimeoutSeconds` to **300 s** when the route does not configure it [S11].
- **[F]** A server that uses the `offer-receipt` extension can add a signed Offer for each `accepts[]` entry, with an optional `validUntil` [S13 §4.1–4.2].

### 2.2 The Permit2 witness signature (step 4) — the only client signature in the standard flow

- **[F]** EIP-712 domain: `{ name: "Permit2", chainId: <from network>, verifyingContract: 0x0000…78BA3 }`. There is no `version` field [S6 L648; S18 L15–17].
- **[F]** Primary type and full type string, as Permit2 reconstructs it on chain [S17 L31–32; S4 L22]:

```
PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)
TokenPermissions(address token,uint256 amount)
Witness(address to,uint256 validAfter)
```

- **[F]** How the reference client fills the fields [S6 L590–629]:

| Field | Value |
|---|---|
| `permitted.token` | `accepted.asset` |
| `permitted.amount` | `accepted.amount` |
| `spender` | the x402 exact proxy (**not** the facilitator) [S1 Phase 2] |
| `nonce` | 256 random bits [S10 L61] |
| `deadline` | `now + maxTimeoutSeconds` |
| `witness.to` | `accepted.payTo` |
| `witness.validAfter` | `0` ("allow some clock skew") |

- **[F]** The wire payload is `{ signature, permit2Authorization: { from, permitted, spender, nonce, deadline, witness } }`. It sits inside a `PaymentPayload` that also **echoes the chosen requirements** as `accepted` [S1 Phase 2; S2 §5.2].

#### What this signature authorizes

- **[F]** It authorizes one pull of exactly `permitted.amount` of `permitted.token` from `from`:
  - only through Permit2;
  - only when `msg.sender == spender`, because Permit2 hashes `msg.sender` into the digest in the spender slot [S17 L93];
  - only to `witness.to`, because the proxy passes `witness.to` as the transfer destination and hashes it into the witness [S4 settle(); S5 L127–135];
  - only while `block.timestamp ≤ deadline` [S16 L60];
  - only when `block.timestamp ≥ validAfter` [S5 L130];
  - at most once, because of the nonce [S16 L63, L150–156].
- **[F]** The exact proxy always requests exactly `permit.permitted.amount` [S4 settle()]. The facilitator cannot change the amount or the recipient [S1 Summary].

#### What it does *not* bind

- **[I]** From the type string above: the signature does **not** cover the resource URL, the HTTP request, the quote identity, the server's identity, or which facilitator will settle it.
  - For comparison: **[F]** the `upto` scheme adds `witness.facilitator` "preventing unauthorized settlement by other parties" [S14 L66]. `exact` does not do this.
- **[I]** Anyone who obtains a valid exact payload before it settles can submit it. The only thing they can achieve is paying the intended `payTo` the intended amount. That cannot steal funds, but it can burn the nonce or front-run the server's settlement.
- **[I]** The signature also does not stop a payee from reusing a payment you signed for request A to gate request B at the same price, because nothing on chain links the payment to a particular request. Only the server's own bookkeeping does. **This gap is what a "quote-binding" second signature would close** (§2.3).

### 2.3 The "second EIP-712 signature" — what exists and what it binds

| Artifact | Signer | Domain | Binds | Enforced where | Source |
|---|---|---|---|---|---|
| Permit2 witness sig | **Client** | Permit2 / chainId / Permit2 address | token, amount, spender(proxy), nonce, deadline, to, validAfter | **On chain** | [S1, S16, S17] |
| Signed Offer (`offer-receipt` ext.) | **Server** | `"x402 offer"`, v"1", **chainId 1 always** | resourceUrl, scheme, network, asset, payTo, amount, validUntil | Off chain, by the client/auditor; the server "MAY reject" when `now > validUntil` | **[F]** [S13 §3.2, §4.3, §4.6] |
| Signed Receipt (same ext.) | **Server** | `"x402 receipt"`, v"1", chainId 1 | network, resourceUrl, payer, issuedAt, optional tx hash | Off chain | **[F]** [S13 §5] |
| `payment-identifier` id | Client (unsigned string) | — | idempotency key. The server should bind it to a fingerprint of scheme/network/asset/amount/payTo/route | Server/facilitator. Same id + different payload → 409 | **[F]** [S12] |

**Using the Offer and `accepted` together as the binding layer**

- **[I]** A client can get most of the "payment ↔ quote" binding like this:
  1. Verify the signed Offer, including the signer-authorization check that [S13 §4.5.1] requires. A valid signature on its own proves nothing about who is entitled to sign.
  2. Check that the Offer fields equal the chosen `accepts[]` entry, and the entry equals the Permit2 fields (`asset = permitted.token`, `amount = permitted.amount`, `payTo = witness.to`).
  3. Set the Permit2 `deadline` to no later than `validUntil`.
  4. Send a `payment-identifier` id so that retries are idempotent.

  Both sides then hold the same signed terms, and the on-chain transfer can only execute those terms.
- **[U]** The spec gives no way to carry an Offer hash inside the Permit2 witness. The exact witness type is fixed at `Witness(address to,uint256 validAfter)` and was reduced post-audit ("post-audit: extra removed from Witness") [S1 Annex, L378]. A binding that is **cryptographically enforced on chain** would need a different proxy or witness type, i.e. a different contract.

### 2.4 Verification (step 6)

**[F]** The spec's ordered checks [S1 Phase 3]:

1. The signature recovers to `from`.
2. `ERC20.allowance(from, Permit2) ≥ amount`. If it is not, a sponsoring extension must be present; otherwise the error is `PERMIT2_ALLOWANCE_REQUIRED`.
3. The balance is sufficient.
4. The amount covers the payment.
5. `deadline` has not passed and `validAfter` is active.
6. Token and network match.
7. Simulate `proxy.settle` (recommended).

**[F]** The reference facilitator is stricter than the spec [S8 L97–222]:

- `spender` must equal the exact proxy;
- `witness.to` must equal `payTo`;
- **`deadline ≥ now + 6 s`** (a 6-second buffer);
- `validAfter ≤ now`;
- amount **exactly equal** to `requirements.amount`;
- token equal to `asset`;
- signature verified the way Permit2 does it: `ecrecover` for code-less addresses, strict EIP-1271 for contracts;
- then a simulation. When the simulation fails, a diagnostic multicall reports whether the proxy is missing, the balance is short, or the allowance is short [S6 `diagnosePermit2SimulationFailure`].

### 2.5 Settlement (steps 8–9) — facilitator pays gas

- **[F]** `/settle` re-runs verification (simulation is off by default) and then calls `x402ExactPermit2Proxy.settle(permit, owner, witness, signature)` [S8 L384–456].
- **[F]** The proxy then:
  - reverts on `amount==0`, `owner==0` or `to==0`;
  - reverts with `PaymentTooEarly` when `now < validAfter`;
  - calls `PERMIT2.permitWitnessTransferFrom` [S5 L127–135].
- **[F]** Permit2 then:
  - reverts `SignatureExpired` when `now > deadline`;
  - reverts `InvalidAmount` when the requested amount exceeds the permitted amount;
  - consumes the nonce;
  - verifies the signature;
  - transfers the tokens [S16 L60–65].
- **[F]** When the transaction is broadcast but its receipt cannot be confirmed, the facilitator MAY return the non-terminal `settlement_pending` with the tx hash. The caller should reconcile on chain before retrying [S2 §9; S1 Phase 4].
- **[F]** Sponsored-approval variants:
  - `settleWithPermit`: an EIP-2612 permit to Permit2, executed atomically inside the proxy. The proxy requires `permit2612.value == permitted amount` [S5 L154] and **does not revert** when the 2612 permit call fails, in case the allowance already exists [S5 `_executePermit`].
  - `erc20ApprovalGasSponsoring`: the facilitator batches a funding transfer, `approve(Permit2)`, then `settle` [S1 Phase 1 B].
- **[I]** On mainnet, the facilitator's gas cost per settlement (a Permit2 transfer through a proxy) is large compared with micro-payments. Expect facilitators to set minimum amounts or fees, or to decline to serve mainnet. The spec does not address this economics question (§9 Q3).

---

## 3. Why the signature cannot be replayed elsewhere

| Replay attempt | What stops it | Source |
|---|---|---|
| Another chain (L2, testnet) | The EIP-712 domain separator includes `block.chainid` and Permit2's address, so a mainnet signature fails verification anywhere else | **[F]** [S18 L27–34] |
| Another spender (facilitator EOA, a malicious contract) | Permit2 hashes `msg.sender` in the spender slot, so only the exact proxy produces a matching digest | **[F]** [S17 L93] |
| Another recipient | `witness.to` is inside the signed witness hash, and the proxy uses it as the destination | **[F]** [S4; S5 L133–135] |
| A larger amount | Permit2 reverts `InvalidAmount` when requested > permitted, and the exact proxy always requests exactly `permitted.amount` | **[F]** [S16 L61; S4] |
| Another token | `permitted.token` is signed | **[F]** [S17] |
| The same payment twice | The unordered nonce bitmap: the bit is flipped on use, and a second use reverts `InvalidNonce` | **[F]** [S16 L142–156] |
| Later, after the job is abandoned | `deadline` | **[F]** [S16 L60] |
| The `upto` proxy or another Permit2 integrator | Different spender address **and** a different witness type string (`upto` adds `facilitator`), so the digest differs | **[F]** [S14 L313]; **[I]** type-string argument |
| Another resource at the same server/price | **Not prevented on chain.** It depends on server bookkeeping and `payment-identifier` request binding | **[I]** from the type string; [S12 Request Binding] |
| Another facilitator submitting it | **Not prevented** for `exact`. The outcome is still "pay `payTo` the signed amount once" | **[I]**; contrast `upto` [S14 L66] |
| The server's signed Offer replayed | `validUntil`, if present; the spec warns about long-lived offers | **[F]** [S13 §10] |

---

## 4. Nonces, deadlines and quote expiry

### 4.1 Nonces

- **[F]** Permit2 `SignatureTransfer` nonces are **unordered**. The upper 248 bits select a 256-bit word in `nonceBitmap[owner]` and the low 8 bits select a bit [S16 L142–145]. Using a nonce XOR-flips its bit, and the call reverts when the bit was already set [S16 L150–156].
- **[F]** The reference client draws 32 random bytes [S10 L61].
- **[I]** Consequences:
  - no sequencing is needed, so parallel payments from one wallet do not block each other;
  - random 256-bit nonces make collisions negligible;
  - **nonces are per owner, not per spender**, so one nonce space is shared by every Permit2 signature-transfer that wallet ever signs, including those for non-x402 apps.
- **[F]** An owner can cancel outstanding signatures ahead of time with `invalidateUnorderedNonces(wordPos, mask)` [S16 L130–134].
  - **[I]** This is the only way to revoke a signed but unsettled payment before its deadline. It costs gas on mainnet.
- **[F]** The nonce is consumed **before** the signature is verified, within the same transaction [S16 L63–65].
  - **[I]** The whole transaction reverts on any failure, so a failed settle does **not** burn the nonce. Only a successful transfer does.

### 4.2 Deadlines and validity window

- **[F]** Permit2 enforces the upper bound: `block.timestamp > deadline` → `SignatureExpired` [S16 L60].
- **[F]** The proxy enforces the lower bound: `block.timestamp < validAfter` → `PaymentTooEarly` [S5 L130].
- **[F]** The reference client uses `validAfter = 0` and `deadline = now + maxTimeoutSeconds` [S6 L600–602].
- **[F]** The reference facilitator rejects `deadline < now + 6` at verify time [S8 L140–147].
- **[I]** The window has to cover all of these:
  1. the time the server spends doing the work in the default `authorization` flow (verify → resource → settle [S2 §6.1]);
  2. `/settle` latency;
  3. mainnet inclusion (~12 s slots, plus congestion).

  A deadline that is too short fails *after* the server has already done the work. The spec leaves the server's handling of that case undefined. One that is too long widens the window during which a leaked payload stays valid.

### 4.3 Quote expiry and how the three clocks interact

- **[F]** There are up to three expiry clocks:
  1. `maxTimeoutSeconds` from the quote [S2 §5.1.2];
  2. the optional Offer `validUntil`, which the server "MAY" enforce [S13 §4.6];
  3. the Permit2 `deadline`, which is enforced on chain.
- **[I]** Recommended rule: `deadline = min(now + maxTimeoutSeconds, offer.validUntil (if non-zero), now + your own policy cap)`.
  - Never sign a deadline later than the quote's own expiry.
  - Treat a quote as dead once `now + expected_work_time + ~30 s (mainnet inclusion margin) > deadline`, and fetch a new 402 instead.
- **[I]** A re-quote must produce a **new nonce and signature**. Resending the old payload after the window has passed fails with `permit2_deadline_expired`.
- **[I]** Clock skew: `validAfter=0` sidesteps skew on the lower bound. On the upper bound, the facilitator's clock (verify) and block time (settle) are what count, not yours.

---

## 5. The concrete ways a payment fails

The error strings are those of the reference TypeScript facilitator [S9]. Other facilitators may use different strings **[U]**.

### 5.1 Missing Permit2 allowance

| | |
|---|---|
| Symptom | Verify returns `permit2_allowance_required`. The spec calls for `412 Precondition Failed` / `PERMIT2_ALLOWANCE_REQUIRED` [S1 Phase 3 step 2]; the SDK returns an `invalidReason` string [S6 `verifyPermit2Allowance`] |
| Cause | `token.allowance(payer, Permit2) < amount`, and no valid `eip2612GasSponsoring` or `erc20ApprovalGasSponsoring` extension is attached |
| Fix | Send `approve(Permit2, X)` on mainnet, paying ETH gas, or attach a sponsoring extension that the server/facilitator advertises. Then retry with a **fresh** signature if the deadline has passed |
| Note | **[U]** The spec says 412, but the HTTP transport table maps a verification failure to 402 [S3 Error Handling]. Handle both |

### 5.2 Insufficient balance

| | |
|---|---|
| Symptom | `permit2_insufficient_balance` (diagnostic) or generic `insufficient_funds` [S9; S2 §9] |
| Cause | Token balance < amount at verify time, **or at settle time**, e.g. a parallel payment drained the wallet between the two |
| Fix | Reserve funds locally before signing (§8). Keep the balance above your in-flight total |

### 5.3 Expired or not-yet-valid window

| | |
|---|---|
| Symptom (verify) | `permit2_deadline_expired` (includes "expires in < 6 s") or `permit2_not_yet_valid` [S8 L140–155] |
| Symptom (settle) | Permit2 `SignatureExpired` or proxy `PaymentTooEarly`, mapped to `invalid_permit2_signature` or `permit2_payment_too_early` [S6 `mapSettleError`] |
| Fix | Re-request the 402 and sign again with a new nonce. Size the deadline as described in §4.2 |
| Note | **[F]** The reference mapper folds `SignatureExpired` into `invalid_permit2_signature` [S6], so an "invalid signature" at settle may actually mean "expired" |

### 5.4 Payment does not match the quote (the "payment hash mismatch")

In the standard flow, a mismatch appears as one of these errors:

- `permit2_amount_mismatch`: amount ≠ quote. It must be **exactly** equal in the reference implementation.
- `invalid_permit2_recipient_mismatch`: `witness.to` ≠ `payTo`.
- `permit2_token_mismatch`.
- `invalid_permit2_spender`: spender ≠ exact proxy.
- `invalid_exact_evm_network_mismatch`.
- `invalid_permit2_signature`: **any** field changed after signing changes the EIP-712 digest, which is the effective "hash mismatch" [S8 L105–222].

Other layers can also report a mismatch:

- `payment-identifier`: the same id with a different payload → **409 Conflict** [S12].
- The signed Offer: if the Offer fields do not equal `accepts[i]`, the client should refuse to pay [S13 §4.1.1].
- **[U]** A proprietary quote-binding signature, if your facilitator uses one, would fail on its own hash comparison. Its format and error codes are not standardized.
- **[I]** Common causes:
  - decimal/string mistakes (`"1.00"` instead of `"1000000"`);
  - checksum or case differences that are not normalized before hashing;
  - sorting `Witness` wrongly in the EIP-712 types;
  - the wrong `chainId` (for example, reusing a Base config);
  - signing against a stale `accepts[]` after the server changed its price.

### 5.5 Reused nonce

| | |
|---|---|
| Symptom | Settle reverts with Permit2 `InvalidNonce`, mapped to `permit2_invalid_nonce` [S16 L155; S6 `mapSettleError`]. Usually caught earlier as `permit2_simulation_failed` at verify time |
| Cause | The same payload is submitted twice (a retry after a successful but unacknowledged settle), a nonce was deliberately reused, or an earlier `invalidateUnorderedNonces` covered it |
| Fix | Before retrying, check on chain whether the prior tx landed (`settlement_pending` carries the hash [S2 §9]). Use `payment-identifier` so the server returns the cached result instead of settling again [S12]. Never recycle nonces |

### 5.6 Other failures to handle

- `permit2_proxy_not_deployed`. **[F]** Not an issue on mainnet today [S19].
- `settlement_pending`. **[F]** Non-terminal: reconcile, do not re-sign blindly [S2 §9].
- `permit2_simulation_failed` (generic).
- `invalid_exact_evm_payload_undeployed_smart_wallet`. **[F]** Relevant to smart-account payers [S9].
- EIP-2612 extension errors (`eip2612_deadline_expired`, `eip2612_spender_not_permit2`, …) [S9].

---

## 6. Pre-flight checklist for client authors (one page)

**Before the first payment (wallet setup)**
- [ ] The wallet is a **dedicated hot wallet** used only by this agent (§7).
- [ ] `allowance(wallet, Permit2)` on the token is set to a **bounded** value, not `MAX_UINT256`. **[F]** The SDK helper `createPermit2ApprovalTx` approves `MAX_UINT256` by default [S7 L13, L61], so override it.
- [ ] The wallet holds a little ETH for approval and `invalidateUnorderedNonces`, or you have confirmed that the server advertises `eip2612GasSponsoring` / `erc20ApprovalGasSponsoring`.
- [ ] Hard-code and pin the addresses: Permit2 `0x000000000022D473030F116dDEE9F6B43aC78BA3`, exact proxy `0x402085c248EeA27D92E8b30b2C58ed07f9E20001`, and an allow-list of token addresses.

**On every 402 challenge**
- [ ] `x402Version == 2`, `scheme == "exact"`, `network == "eip155:1"`, and `extra.assetTransferMethod == "permit2"`. Skip any `paymentFlow` you do not recognize [S2 §6.1].
- [ ] `asset` is on your allow-list. `amount` is parsed as an integer in atomic units, **not** a float.
- [ ] `amount ≤ per-payment cap`, and `amount + reserved in-flight + spent today ≤ daily budget`.
- [ ] `payTo` is on the allow-list, or matches the verified signed Offer and its signer authorization [S13 §4.5.1].
- [ ] If an Offer is present: its fields equal `accepts[i]` and `validUntil` has not passed.
- [ ] Balance ≥ amount + in-flight reservations. Allowance to Permit2 ≥ amount.

**When signing**
- [ ] Domain `{name:"Permit2", chainId:1, verifyingContract:Permit2}` with no `version`.
- [ ] `spender` = the exact proxy. `witness.to` = `payTo`. `permitted` = {asset, amount}, both copied exactly.
- [ ] `nonce` = 32 CSPRNG bytes; never reused, never derived from a counter visible to others.
- [ ] `deadline` = min(now+`maxTimeoutSeconds`, `validUntil`, now+policy max), and more than 6 s + expected work + inclusion margin into the future.
- [ ] `validAfter = 0` unless you have a reason to delay.
- [ ] Record `{payment-id, nonce, deadline, amount, payTo, resource}` in a local ledger **before** sending.

**When sending and afterwards**
- [ ] Send `PAYMENT-SIGNATURE` over HTTPS only, and include a `payment-identifier` id. Reuse the same id on retries [S12].
- [ ] On `settlement_pending`: look up the tx hash on chain before doing anything else.
- [ ] On an expiry or mismatch error: re-fetch the 402 and re-sign with a **new nonce**. Never edit a signed payload.
- [ ] On `permit2_allowance_required`: stop and ask for approval (escalate to the operator). Do not approve automatically without a limit.
- [ ] Decode `PAYMENT-RESPONSE` and check that `transaction` exists on chain with the expected `Transfer(from, payTo, amount)`.
- [ ] Release or commit the budget reservation according to the final on-chain outcome.

---

## 7. Spending controls the agent operator should enforce

- **[F]** The core spec states that "client-side budget management" is **out of scope** [S2 Document Scope] and describes budget controls as "implementation-specific" [S2 §12.1].
- **[I]** Everything below is therefore operator responsibility. It comes from the mechanics above, not from a standard.

1. **Per-payment cap.**
   - Reject any `accepts[]` entry whose `amount` exceeds a hard cap before signing. Enforce it in the signer and not only in the agent prompt or planner, because the signature is the last point you control.
   - Add a daily or rolling cap, a per-`payTo` cap, and a rate limit on the number of signatures per minute.
2. **Bounded allowances.**
   - `ERC20.approve(Permit2, X)` gives the Permit2 contract the right to move up to X. Any valid Permit2 signature from this wallet, for **any** spender or app, can draw on it.
   - Set X to roughly one day's budget and top it up deliberately. Do not use the SDK's `MAX_UINT256` default [S7].
   - Keep in mind that the balance, not the allowance, is the real ceiling. That is why the wallet is kept small.
3. **Dedicated hot wallet with a small balance.**
   - Use one key per agent (or per agent × environment), funded with a working float of perhaps a few days of expected spend, and refilled from cold storage by a separate, human-gated process.
   - A compromised key then loses at most the float, even though Permit2 signatures carry no scope beyond each payment.
   - Monitor outgoing `Transfer` events and alert on any `to` that is not on the allow-list.
4. **Short deadlines and revocation readiness.**
   - Keep deadlines short (§4.2).
   - Keep an ops runbook for `invalidateUnorderedNonces` so that signed-but-unsettled payments can be killed, and for `approve(Permit2, 0)` to shut off all Permit2 pulls.
5. **Sign-only boundary.**
   - Keep the private key in a signer service or KMS that only signs `PermitWitnessTransferFrom` where `spender = exact proxy`, `chainId = 1`, the token is on the allow-list and the amount is within the cap.
   - It should refuse arbitrary typed data. The signer does not need to know about HTTP.

---

## 8. Worked example: sizing numbers (illustrative only)

**[I]** An agent pays ≈ $0.05 per call in USDC (50,000 atomic units) and makes ≈ 2,000 calls a day.

- Per-payment cap: 100,000.
- Daily budget: $150 (150,000,000).
- Permit2 allowance: 150,000,000, refreshed daily.
- Hot-wallet float: $300 plus about 0.01 ETH for approvals.
- Deadline: ≤ 120 s for jobs expected to take < 60 s.

These numbers are policy choices, not protocol values.

---

## 9. Open questions and known limits

- **[Q1]** Is a specific facilitator or vendor behind the brief's "second EIP-712 signature binding the payment to the quote" and "payment hash"? I found no such client-side signature in the x402 v2 specs, extensions or reference SDKs at `4fcf836` [S1–S3, S12–S14]. If a vendor defines one, obtain its type string, domain, and the rule for combining it with the Permit2 nonce and deadline.
- **[Q2]** Which production facilitators currently list `eip155:1` + `exact` + Permit2 in `/supported`? The SDK can register mainnet [S8 register example; S20], but I did not query any live facilitator's `/supported` endpoint.
- **[Q3]** What are mainnet fee and minimum-amount policies of facilitators, given L1 gas? This is not specified anywhere.
- **[U4]** Status code for a missing allowance: the scheme text says 412, the transport table says 402, and the SDK returns an `invalidReason` string. Clients should handle all three.
- **[U5]** Behaviour of the verify → work → settle race when the deadline passes during the work: the spec does not say whether the server must still deliver or should refund. This is a server policy question.
- **[U6]** Tokens with non-standard `approve` semantics (for example, some tokens require setting the allowance to 0 before changing a non-zero value) affect "top up the bounded allowance" procedures. I did not verify this for specific mainnet tokens here.
- **Limits of this research:**
  - The research relies on specs and reference code at pinned commits and on two on-chain reads.
  - No live payment was executed.
  - No third-party facilitator was tested.
  - The audit status of the proxy is taken from the spec's statement that it is "audited, battle-tested" [S1 Implementer Notes] and was not verified independently.

---

## 10. Sources

X = `https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3`
P = `https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219`

- **S1** x402 `exact` scheme on EVM (Permit2 phases, proxy reference, canonical addresses) — X/specs/schemes/exact/scheme_exact_evm.md — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/specs/schemes/exact/scheme_exact_evm.md
- **S2** x402 Protocol Specification v2 (§5 types, §6.1 flows, §7 facilitator API, §9 errors, §10 security, scope) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/specs/x402-specification-v2.md
- **S3** x402 v2 HTTP transport (`PAYMENT-REQUIRED`/`PAYMENT-SIGNATURE`/`PAYMENT-RESPONSE`, status mapping) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/specs/transports-v2/http.md
- **S4** `x402ExactPermit2Proxy.sol` — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/contracts/evm/src/x402ExactPermit2Proxy.sol
- **S5** `x402BasePermit2Proxy.sol` (`_settle`, `_executePermit`) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/contracts/evm/src/x402BasePermit2Proxy.sol
- **S6** Reference TS shared Permit2 logic (client payload construction L590–629, signing domain L648, allowance check, error mapping, diagnostics); constants — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/mechanisms/evm/src/shared/permit2.ts ; S6-const: https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/mechanisms/evm/src/constants.ts
- **S7** Reference TS client Permit2 helpers (`createPermit2ApprovalTx` uses `MAX_UINT256`) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts
- **S8** Reference TS facilitator Permit2 verify/settle — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts
- **S9** Reference facilitator error codes — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts
- **S10** Nonce generation `createPermit2Nonce` (L61) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/mechanisms/evm/src/utils.ts#L61
- **S11** Server default `maxTimeoutSeconds` 300 (L813) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/core/src/server/x402ResourceServer.ts#L813
- **S12** `payment-identifier` extension (idempotency, request binding, 409) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/specs/extensions/payment_identifier.md
- **S13** `offer-receipt` extension (server-signed EIP-712 Offer/Receipt, `validUntil`, signer authorization) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/specs/extensions/extension-offer-and-receipt.md
- **S14** `upto` scheme on EVM (witness includes `facilitator`; separate proxy) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/specs/schemes/upto/scheme_upto_evm.md
- **S15** `auth-capture` scheme on EVM (payment-info hashes, salt binding; a different scheme) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/specs/schemes/auth-capture/scheme_auth_capture_evm.md
- **S16** Permit2 `SignatureTransfer.sol` (deadline L60, amount L61, nonce L63/L142–156, invalidate L130) — https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/SignatureTransfer.sol
- **S17** Permit2 `PermitHash.sol` (witness typehash stub L31–32, `msg.sender` as spender L93) — https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/libraries/PermitHash.sol
- **S18** Permit2 `EIP712.sol` (domain with `chainid` and contract address) — https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/EIP712.sol
- **S19** On-chain observation, Ethereum mainnet via `https://ethereum-rpc.publicnode.com`, block 26,056,849 (2026-09-25 20:27:35 UTC):
  - `eth_getCode` for `0x000000000022D473030F116dDEE9F6B43aC78BA3` → non-empty (9,152 bytes);
  - `eth_getCode` for `0x402085c248EeA27D92E8b30b2C58ed07f9E20001` → non-empty (2,913 bytes);
  - `eth_call` `PERMIT2()` (selector `0x6afdd850`) on the proxy → `0x…22d473030f116ddee9f6b43ac78ba3`.

  These results can be reproduced with any mainnet RPC.
- **S20** SDK default assets (mainnet USDC, L59) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/typescript/packages/mechanisms/evm/src/defaultAssets.ts#L59
- **S21** EIP-712 Typed structured data hashing and signing — https://eips.ethereum.org/EIPS/eip-712
- **S22** Uniswap canonical deployments (Permit2 address reference cited by S1) — https://docs.uniswap.org/contracts/v4/deployments
- **S23** e2e test facilitator `createPaymentHash` (sha256 of payload, 5-min verify→settle window; test code, not protocol) — https://github.com/x402-foundation/x402/blob/4fcf836cc393174130e1358577ce5d37356da1c3/e2e/facilitators/typescript/index.ts#L528
