EVM Asset Transfer Methods
How EIP-3009 and Permit2 transfers work in x402 on EVM chains
On EVM networks, x402 supports two ways to move ERC-20 tokens gaslessly: EIP-3009 and Permit2. Both let the client authorize a transfer by signing a message off-chain, while the facilitator submits the resulting transaction and pays the gas. The choice depends on whether the token natively supports transferWithAuthorization.
Background: Vanilla ERC-20
A standard ERC-20 token only has transfer() and transferFrom(). To allow a third party (like a facilitator) to move your tokens, you must first submit an on-chain approve() transaction — paying gas. After that, the approved address can call transferFrom() on your behalf.
This two-step process (approve + transferFrom) is not ideal for x402 because:
- The client must hold native gas tokens to submit the approval.
- Each approval is a separate on-chain transaction.
- The approved spender can move up to the approved amount at any time — with no per-transfer authorization.
Both EIP-3009 and Permit2 solve this by moving the authorization step off-chain.
EIP-3009: transferWithAuthorization
EIP-3009 extends ERC-20 with a function that accepts a signed message authorizing a single transfer. The function is built into the token contract itself.
How It Works
sequenceDiagram
participant Client
participant Seller as Resource Server (Seller)
participant Facilitator
participant Token as ERC-20 Contract (EIP-3009)
Client->>Seller: 1. Request resource
Seller-->>Client: 2. 402 Payment Required (with PaymentRequirements)
Note over Client: 3. Sign EIP-712 TransferWithAuthorization
Client->>Seller: 4. Retry with signed PaymentPayload
Seller->>Facilitator: 5. Verify signature, balance, simulate
Facilitator-->>Seller: 6. Valid
Note over Seller: 7. Fulfill request
Seller->>Facilitator: 8. Settle
Facilitator->>Token: 9. transferWithAuthorization(...)
Token-->>Facilitator: 10. Transfer executed
Facilitator-->>Seller: 11. Settlement confirmed (tx hash)
Seller-->>Client: 12. Response + PAYMENT-RESPONSE header
Signature Structure
The client signs EIP-712 typed data with these fields:
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
]
The EIP-712 domain requires name and version to match the values hardcoded in the token contract (e.g., name: "USDC", version: "2" for USDC). These are passed in the extra field of PaymentRequirements.
Security Properties
- Per-transfer authorization: Each signature authorizes exactly one transfer of a specific amount to a specific recipient. It cannot be reused after execution.
- Time-bounded:
validAfterandvalidBeforedefine the window during which the authorization can be executed. - Unique nonce: A random 32-byte nonce prevents replay attacks, enforced at the smart contract level.
- Facilitator cannot alter anything: Amount, recipient, and sender are all part of the signed message. Any change invalidates the signature.
extra Fields
| Field | Required | Default | Description |
|---|---|---|---|
name | Recommended | — | EIP-712 domain name from the token contract |
version | Recommended | — | EIP-712 domain version from the token contract |
assetTransferMethod | No | eip3009 | Can be omitted — eip3009 is the default value |
If name and version are omitted, the facilitator will query the token contract to determine them. This adds an extra RPC call and is less efficient, but functionally equivalent.
Supported Tokens
EIP-3009 is implemented by a limited set of tokens. Known examples:
| Token | Networks |
|---|---|
| USDC | Base, Polygon, Avalanche, and other supported EVM chains |
| EURC | Base, Avalanche |
Permit2: Universal ERC-20 Support
For tokens that do not implement EIP-3009, x402 uses Uniswap’s Permit2 — a canonical contract deployed at the same address on all major EVM chains.
Permit2 introduces a signature-based transfer mechanism for any ERC-20 token. However, it requires a one-time on-chain approval to the Permit2 contract.
One-Time Setup
Before a client can use Permit2-based payments, they must approve the Permit2 contract to spend their tokens:
token.approve(PERMIT2_ADDRESS, type(uint256).max)
This is a single on-chain transaction that pays gas. After this, all subsequent x402 payments with that token are gasless.
If the facilitator supports it, even this one-time approval can be gasless — see the eip2612GasSponsoring and erc20ApprovalGasSponsoring extensions in the x402 specification.
Why x402Permit2Proxy?
If the facilitator called Permit2 directly, the spender in the Permit2 signature would be the facilitator’s address. Permit2’s permitTransferFrom lets the spender choose where to send the tokens and how much. This means a malicious or compromised facilitator could redirect funds or change the amount.
The x402Permit2Proxy contract solves this by acting as the spender instead of the facilitator. The proxy contract uses Permit2’s Witness pattern — extra data included in the signed message that the proxy enforces on-chain:
struct Witness {
address to; // Destination address — immutable once signed
uint256 validAfter; // Earliest time the payment can be settled
bytes extra; // Reserved for extensions
}
The proxy contract reads to from the witness and uses it as the transfer destination. The facilitator has no ability to override this because:
- The
toaddress is part of the signed message. Changing it invalidates the signature. - The proxy contract is the only authorized
spenderin the Permit2 signature. The facilitator calls the proxy, not Permit2 directly.
How It Works (Exact Scheme)
sequenceDiagram
participant Client
participant Seller as Resource Server (Seller)
participant Facilitator
participant Proxy as x402ExactPermit2Proxy
participant P2 as Permit2 Contract
participant Token as ERC-20 Contract
Note over Client: Prerequisite: token.approve(Permit2, MAX) — one time
Client->>Seller: 1. Request resource
Seller-->>Client: 2. 402 Payment Required
Note over Client: 3. Sign permitWitnessTransferFrom<br/>(spender = Proxy, witness.to = payTo)
Client->>Seller: 4. Retry with signed PaymentPayload
Seller->>Facilitator: 5. Verify
Facilitator-->>Seller: 6. Valid
Note over Seller: 7. Fulfill request
Seller->>Facilitator: 8. Settle
Facilitator->>Proxy: 9. settle(...)
Proxy->>P2: 10. permitWitnessTransferFrom(...)
P2->>Token: 11. transferFrom(...)
Token-->>P2: 12. Transfer executed
P2-->>Proxy: 13. Success
Proxy-->>Facilitator: 14. Settled
Facilitator-->>Seller: 15. Settlement confirmed (tx hash)
Seller-->>Client: 16. Response
Why the Facilitator Cannot Cheat
The trust model is designed so the facilitator is a transaction broadcaster, not a custodian:
| Attack Vector | Protection |
|---|---|
| Change destination address | witness.to is signed by the client and enforced by the proxy contract |
| Call Permit2 directly | The client’s signature specifies the proxy as the spender, not the facilitator |
| Replay the signature | Permit2 nonces are single-use; the contract rejects already-used nonces |
| Delay settlement indefinitely | deadline in the Permit2 signature enforces an upper time bound; witness.validAfter enforces a lower bound |
extra Fields
| Field | Required | Description |
|---|---|---|
assetTransferMethod | Yes | Must be permit2 |
PaymentPayload Structure
When assetTransferMethod=permit2, the payload contains permit2Authorization instead of authorization:
{
"payload": {
"signature": "0x...",
"permit2Authorization": {
"permitted": {
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"amount": "10000"
},
"from": "0x857b06519E91e3A54538791bDbb0E22373e36b66",
"spender": "0x<x402Permit2Proxy address>",
"nonce": "0xf374...3480",
"deadline": "1740672154",
"witness": {
"to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
"validAfter": "1740672089",
"extra": {}
}
}
}
}
Key differences from EIP-3009 payload:
spenderis thex402Permit2Proxycontract, not the facilitator.witness.todetermines where funds go — enforced by the proxy, not by the facilitator.deadlinereplacesvalidBeforeas the upper time bound.
Exact vs Upto (Permit2 only)
The exact scheme always transfers the full authorized amount. The upto scheme allows settling for less or equal than the authorized maximum — useful for usage-based pricing (LLM token charges, bandwidth metering, etc.). For a comprehensive guide to the upto scheme, see the Upto Scheme documentation.
| Scheme | assetTransferMethod | Amount settled | Proxy contract |
|---|---|---|---|
exact | eip3009 | Exactly authorization.value | None (direct token call) |
exact | permit2 | Exactly permit.permitted.amount | x402ExactPermit2Proxy |
upto | permit2 only | Any amount ≤ permit.permitted.amount | x402UptoPermit2Proxy |
EIP-3009 does not support the upto scheme. transferWithAuthorization requires the exact amount at signature time — there is no way to settle for less.
Choosing the Right Method
| Criteria | EIP-3009 | Permit2 |
|---|---|---|
| Token support | Only tokens with EIP-3009 (USDC, EURC, etc.) | Any ERC-20 token |
| Client setup | None | One-time approve() to Permit2 |
| Gas for client | Zero | One-time gas for approval |
| Payload complexity | Lower | Higher (Permit2 + Witness) |
| Smart contract trust | Token contract only | Permit2 + x402Permit2Proxy |
| Scheme support | exact only | exact and upto |
| Default in x402 | Yes (assetTransferMethod default) | Must be explicitly set |
For most integrations using USDC or EURC, EIP-3009 is the simplest path. For other ERC-20 tokens, or when you need the upto scheme, use Permit2.