Permit vs TransferWithAuthorization

Gasless payment signatures: EIP-2612 permit and EIP-3009 transferWithAuthorization

Overview

AnySpend x402 uses cryptographic signatures instead of traditional token approvals, enabling gasless payments for users. The facilitator pays all gas fees while users simply sign authorization messages.

There are two main signature standards supported, each optimized for different token types:

  • EIP-3009 (transferWithAuthorization) - Used by USDC, one-step direct transfers
  • EIP-2612 (permit) - Used by most modern ERC-20 tokens, two-step approval + transfer

Quick comparison

FeatureEIP-2612 (Permit)EIP-3009 (transferWithAuthorization)
Used byMost modern ERC-20sUSDC (all networks)
ExecutionTwo-step: approve + transferOne-step: direct transfer
Nonce TypeSequential (auto-increments)Random bytes32
DependencyMust wait for previous nonceNo ordering required
Ideal forGeneral ERC-20 tokensStablecoin payments
Replay ProtectionSequential nonceRandom nonce tracking
Gas Efficiency2 transactions (permit + transferFrom)1 transaction (direct transfer)

EIP-3009: transferWithAuthorization (USDC)

Overview

Direct transfer authorization - the signature authorizes an immediate transfer from sender to recipient without a separate approval step.

Used by: USDC on all networks (Base, Ethereum, Arbitrum, Optimism, Polygon, etc.)

Key advantages

  • One-step execution - direct transfer, no approval needed
  • Random nonce - no sequential dependency, parallel transactions possible
  • Immediate settlement - executes in single transaction
  • Gasless for payer - facilitator pays gas
  • No front-running - random nonce prevents MEV attacks

Message structure

solidity
transferWithAuthorization( address from, // Payer address address to, // Recipient address uint256 value, // Amount to transfer uint256 validAfter, // 0 (valid immediately) uint256 validBefore, // Deadline timestamp bytes32 nonce, // Random nonce (prevents replay) bytes signature // EIP-712 signature )

Usage example

typescript
import { signTransferWithAuthorization } from 'anyspend-x402-client'; const signature = await signTransferWithAuthorization({ from: payerAddress, to: recipientAddress, value: '1000000', // 1 USDC (6 decimals) validBefore: deadline, nonce: randomBytes32() // Generate random nonce });

How it works

User Signs Authorization

User signs an EIP-712 message authorizing the transfer with a random nonce

Facilitator Executes Transfer

Facilitator calls receiveWithAuthorization() with the signature

USDC Transferred

USDC is transferred directly from user to facilitator in one atomic transaction

Nonce Invalidated

The random nonce is marked as used, preventing replay attacks

EIP-712 typed data structure

typescript
const typedData = { domain: { name: 'USD Coin', version: '2', chainId: 8453, // Base verifyingContract: usdcAddress }, 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' } ] }, message: { from: payerAddress, to: recipientAddress, value: '1000000', validAfter: 0, validBefore: deadline, nonce: randomNonce } };

EIP-2612: Permit (Standard ERC-20)

Overview

Signature-based approval that sets an allowance, followed by a separate transferFrom() call. This is the standard method for most modern ERC-20 tokens.

Used by: Most modern ERC-20 tokens with permit support

Key advantages

  • Widely adopted - standard across many tokens
  • Time-limited approvals - deadline-based expiration
  • ERC-20 compatible - works with existing infrastructure
  • Gasless for payer - facilitator pays gas
  • Supported by major wallets and dapps

Message structure

solidity
permit( address owner, // Token owner address spender, // Approved spender (facilitator) uint256 value, // Approval amount uint256 deadline, // Expiration timestamp uint8 v, bytes32 r, bytes32 s // Signature components )

Usage example

typescript
import { signPermit } from 'anyspend-x402-client'; const nonce = await token.nonces(ownerAddress); // Get current nonce const signature = await signPermit({ token: tokenAddress, owner: ownerAddress, spender: facilitatorAddress, value: '1000000', // 1 USDC (6 decimals) deadline: deadline, nonce: nonce // Sequential nonce });

How it works

User Signs Permit

User signs an EIP-712 permit message with current sequential nonce

Facilitator Calls Permit

Facilitator calls permit() to set the allowance on-chain

Nonce Auto-Increments

The token contract automatically increments the user's nonce

Facilitator Transfers Tokens

Facilitator calls transferFrom() to transfer tokens using the approval

EIP-712 typed data structure

typescript
const typedData = { domain: { name: 'Dai Stablecoin', version: '1', chainId: 8453, // Base verifyingContract: daiAddress }, types: { Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' } ] }, message: { owner: ownerAddress, spender: facilitatorAddress, value: '1000000000000000000', nonce: currentNonce, deadline: deadline } };

Which method does my token use?

Checking token support

typescript
// Check if token supports EIP-3009 (USDC-style) const hasTransferWithAuth = await token.read.transferWithAuthorization !== undefined; // Check if token supports EIP-2612 (Standard permit) const hasPermit = await token.read.permit !== undefined; const hasDomainSeparator = await token.read.DOMAIN_SEPARATOR !== undefined;

Common tokens by method

EIP-3009 (transferWithAuthorization):

  • USDC (all chains)
  • USDC.e (bridged versions)

EIP-2612 (permit):

  • Most modern ERC-20s
  • USDT (on some chains - Base, Arbitrum, Optimism)
  • Many DeFi tokens
  • Check compatibility: anyspend.com/x402-tokens

No Gasless Support:

  • USDT on Ethereum mainnet (no permit)
  • USDT on Polygon (no permit)
  • Legacy ERC-20 tokens
Note

The AnySpend x402 client automatically detects which signature method to use based on the token contract. You don't need to specify this manually.

Nonce management

Random nonce (EIP-3009)

Advantages:

  • No ordering dependency - multiple signatures can be used in any order
  • Parallel transactions possible
  • No blocked state if one transaction fails

Implementation:

typescript
import { randomBytes } from 'crypto'; // Generate cryptographically secure random nonce const nonce = '0x' + randomBytes(32).toString('hex');

Nonce Tracking:

solidity
// Check if nonce has been used mapping(address => mapping(bytes32 => bool)) public authorizationState; function isAuthorizationUsed(address authorizer, bytes32 nonce) external view returns (bool) { return authorizationState[authorizer][nonce]; }

Sequential nonce (EIP-2612)

Advantages:

  • Simple and predictable
  • Gas efficient (single storage slot)
  • Standard across all permit implementations

Implementation:

typescript
// Get current nonce from token contract const currentNonce = await token.read.nonces([ownerAddress]); // Sign with current nonce const signature = await signPermit({ // ... nonce: currentNonce });

Nonce Auto-Increment:

solidity
// Token contract automatically increments mapping(address => uint256) public nonces; function permit(/* ... */) external { require(nonce == nonces[owner], "Invalid nonce"); nonces[owner]++; // Auto-increment // ... rest of permit logic }

Security considerations

Replay protection

EIP-3009:

  • Random nonce prevents replay across chains and contracts
  • Each nonce can only be used once per address
  • Nonce state stored on-chain in mapping

EIP-2612:

  • Sequential nonce prevents replay
  • Must use current nonce (auto-increments)
  • Failed transactions block subsequent signatures until re-signed

Deadline enforcement

Both methods enforce deadlines to prevent stale signatures:

typescript
const deadline = Math.floor(Date.now() / 1000) + 300; // 5 minutes from now

Best Practices:

  • Use short deadlines (5-10 minutes) for security
  • Longer deadlines (30-60 minutes) for better UX if needed
  • Never use type(uint256).max for infinite approvals

Signature validation

Both methods validate signatures using EIP-712:

typescript
// Recover signer from signature const recoveredAddress = recoverTypedDataAddress({ domain, types, primaryType, message, signature }); // Verify signer matches expected address if (recoveredAddress !== expectedSigner) { throw new Error('Invalid signature'); }

Client SDK integration

The AnySpend x402 client handles all signature complexity automatically:

typescript
import { X402Client } from 'anyspend-x402-client'; const client = new X402Client({ walletClient, preferredToken: tokenAddress // USDC or any compatible token }); // Client automatically: // 1. Detects if token uses permit or transferWithAuthorization // 2. Gets current nonce (for permit) or generates random nonce // 3. Constructs correct EIP-712 typed data // 4. Prompts user to sign // 5. Includes signature in X-PAYMENT header const response = await client.request('https://api.example.com/data');

Gas cost comparison

MethodUser GasFacilitator GasTotal Transactions
EIP-30090~45,0001
EIP-26120~70,000 (permit) + ~45,000 (transfer)2
Note

All gas costs are paid by the facilitator and included in the 0.25% AnySpend fee. Users never pay gas directly.

Further reading

EIP-2612 Specification

Official EIP-2612 permit specification

EIP-3009 Specification

Official EIP-3009 transferWithAuthorization specification

EIP-712 Typed Data

EIP-712 typed structured data hashing and signing

Network Support

See which tokens support which methods on each network

Learn More
Ask a question... ⌘I