Core Concepts 10 min read

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: validAfter and validBefore define 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

FieldRequiredDefaultDescription
nameRecommendedEIP-712 domain name from the token contract
versionRecommendedEIP-712 domain version from the token contract
assetTransferMethodNoeip3009Can be omitted — eip3009 is the default value
Note

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:

TokenNetworks
USDCBase, Polygon, Avalanche, and other supported EVM chains
EURCBase, 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.

Gasless Approval

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:

  1. The to address is part of the signed message. Changing it invalidates the signature.
  2. The proxy contract is the only authorized spender in 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 VectorProtection
Change destination addresswitness.to is signed by the client and enforced by the proxy contract
Call Permit2 directlyThe client’s signature specifies the proxy as the spender, not the facilitator
Replay the signaturePermit2 nonces are single-use; the contract rejects already-used nonces
Delay settlement indefinitelydeadline in the Permit2 signature enforces an upper time bound; witness.validAfter enforces a lower bound

extra Fields

FieldRequiredDescription
assetTransferMethodYesMust 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:

  • spender is the x402Permit2Proxy contract, not the facilitator.
  • witness.to determines where funds go — enforced by the proxy, not by the facilitator.
  • deadline replaces validBefore as 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.

SchemeassetTransferMethodAmount settledProxy contract
exacteip3009Exactly authorization.valueNone (direct token call)
exactpermit2Exactly permit.permitted.amountx402ExactPermit2Proxy
uptopermit2 onlyAny amount ≤ permit.permitted.amountx402UptoPermit2Proxy
Important

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

CriteriaEIP-3009Permit2
Token supportOnly tokens with EIP-3009 (USDC, EURC, etc.)Any ERC-20 token
Client setupNoneOne-time approve() to Permit2
Gas for clientZeroOne-time gas for approval
Payload complexityLowerHigher (Permit2 + Witness)
Smart contract trustToken contract onlyPermit2 + x402Permit2Proxy
Scheme supportexact onlyexact and upto
Default in x402Yes (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.

Further Reading