x402 ERC-3009 USDC Ethereum meta-transactions

ERC-3009: The Protocol Powering x402 Payments

A deep dive into ERC-3009 Transfer With Authorization - the cryptographic standard that enables gasless, signature-based token transfers in the x402 protocol.

PayIn Team | | 6 min read

ERC-3009

At the heart of x402’s payment mechanism lies ERC-3009, a standard that enables gasless token transfers through cryptographic signatures. Understanding this protocol is key to grasping how x402 achieves frictionless payments without requiring users to hold native tokens for gas.

The Problem: Traditional ERC-20 Limitations

The standard ERC-20 approve/transferFrom pattern has significant UX problems:

  1. Two transactions required: First approve, then transferFrom
  2. Two gas fees: Users pay twice
  3. Native token dependency: Must hold ETH (or the chain’s native token) just to move USDC
  4. Sequential nonces: Transactions must be ordered, creating bottlenecks

For AI agents and autonomous systems, these limitations are even more severe—an agent would need to manage multiple token balances across networks just to make payments.

ERC-3009: Transfer With Authorization

ERC-3009 solves these problems elegantly. Instead of on-chain approvals, the payer signs an off-chain authorization message that can be submitted by anyone.

The Core Functions

function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external;

function receiveWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external;

How It Works

┌─────────────┐    Sign EIP-712    ┌─────────────┐
│   Payer     │ ─────────────────► │  Message    │
└─────────────┘                    └──────┬──────┘


┌─────────────┐   Submit to chain  ┌─────────────┐
│  Relayer/   │ ◄───────────────── │  Signature  │
│ Facilitator │                    └─────────────┘
└──────┬──────┘


┌─────────────┐
│   USDC      │  transferWithAuthorization()
│  Contract   │  - Verify signature
└──────┬──────┘  - Check balance
       │         - Execute transfer

┌─────────────┐
│  Recipient  │  Receives USDC
└─────────────┘
  1. Payer signs a message conforming to EIP-712 typed data
  2. Message is sent to a relayer or facilitator (off-chain)
  3. Relayer submits the signature on-chain via transferWithAuthorization
  4. Contract verifies the signature and executes the transfer
  5. Relayer pays gas, not the payer

Why Random Nonces Matter

One of ERC-3009’s key innovations is the use of random 32-byte nonces instead of sequential ones:

ApproachERC-2612 (Sequential)ERC-3009 (Random)
Nonce0, 1, 2, 3…Random bytes32
Parallel ops❌ Must be ordered✅ Fully independent
High-frequency❌ Bottleneck✅ Scales infinitely
RecoveryComplexSimple

For x402 and AI agents, this is transformative. An agent can generate thousands of concurrent payment authorizations without any conflicts or ordering requirements.

ERC-3009 in x402

In the x402 protocol, ERC-3009 is the foundation for EVM payments:

Payment Flow

  1. Client requests a protected resource
  2. Server returns 402 with payment requirements
  3. Client constructs an ERC-3009 authorization message
  4. Client signs with their wallet
  5. Client retries with X-PAYMENT header containing the signed payload
  6. Facilitator verifies the signature and balance
  7. Server responds with the content
  8. Facilitator settles by calling transferWithAuthorization

The X-PAYMENT Payload

{
  "x402Version": 1,
  "scheme": "exact",
  "network": "base-sepolia",
  "payload": {
    "signature": "0x...",
    "authorization": {
      "from": "0xPayerAddress",
      "to": "0xRecipientAddress",
      "value": "1000000",
      "validAfter": 0,
      "validBefore": 1734567890,
      "nonce": "0x7a8b9c..."
    }
  }
}

EIP-712 Typed Data Structure

The signed message follows EIP-712 format:

const domain = {
  name: "USD Coin",
  version: "2",
  chainId: 84532, // Base Sepolia
  verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
};

const types = {
  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" }
  ]
};

const message = {
  from: payerAddress,
  to: recipientAddress,
  value: amount,
  validAfter: 0,
  validBefore: Math.floor(Date.now() / 1000) + 3600,
  nonce: randomBytes32()
};

Token Support

ERC-3009 is not universally implemented. Currently, the major tokens supporting it are:

TokenIssuerERC-3009 Support
USDCCircle✅ Yes (v2+)
EURCCircle✅ Yes
USDTTether❌ No
DAIMakerDAO❌ No

This is precisely why x402 focuses on USDC rather than USDT—the underlying smart contract must support the authorization pattern.

Security Considerations

1. Front-Running Protection

Use receiveWithAuthorization instead of transferWithAuthorization when the recipient is a smart contract:

// Vulnerable to front-running
transferWithAuthorization(from, to, value, ...);

// Protected - requires caller == to
receiveWithAuthorization(from, to, value, ...);

An attacker could extract the authorization from the mempool and execute it before the intended recipient’s wrapper contract, potentially causing deposit processing issues.

2. Time-Bound Validity

Always set reasonable validAfter and validBefore windows:

const authorization = {
  validAfter: Math.floor(Date.now() / 1000) - 60,    // 1 minute ago
  validBefore: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
};

3. Nonce Management

Each nonce can only be used once. The contract tracks used nonces:

mapping(address => mapping(bytes32 => bool)) private _authorizationStates;

Generate truly random nonces to avoid collisions:

import { randomBytes } from "crypto";
const nonce = "0x" + randomBytes(32).toString("hex");

Comparison with ERC-2612 (Permit)

FeatureERC-2612ERC-3009
PurposeApprove allowanceDirect transfer
StepsSign → Approve → TransferSign → Transfer
Nonce typeSequentialRandom
Use caseDeFi protocolsPayments, x402
Adopted byDAI, many tokensUSDC, EURC

ERC-3009 is more suitable for payment scenarios because it:

  • Authorizes the exact transfer (not an allowance that could be drained)
  • Supports parallel operations via random nonces
  • Provides time-bounded validity windows

Implementation Example

Using viem to create and sign an ERC-3009 authorization:

import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";
import { randomBytes } from "crypto";

const account = privateKeyToAccount(PRIVATE_KEY);
const client = createWalletClient({
  account,
  chain: baseSepolia,
  transport: http(),
});

const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";

async function createAuthorization(to: string, value: bigint) {
  const nonce = "0x" + randomBytes(32).toString("hex");
  const validBefore = Math.floor(Date.now() / 1000) + 3600;

  const signature = await client.signTypedData({
    domain: {
      name: "USD Coin",
      version: "2",
      chainId: baseSepolia.id,
      verifyingContract: USDC_ADDRESS,
    },
    types: {
      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" },
      ],
    },
    primaryType: "TransferWithAuthorization",
    message: {
      from: account.address,
      to,
      value,
      validAfter: 0n,
      validBefore: BigInt(validBefore),
      nonce,
    },
  });

  return { signature, nonce, validBefore };
}

References


ERC-3009 transforms how we think about token transfers. By moving authorization off-chain, it enables the frictionless, gasless payments that make x402 possible.