Skip to main content

Overview

The EVM payment handler implements the exact scheme for x402-exact payments on Ethereum Virtual Machine (EVM) compatible networks. It uses EIP-3009 transferWithAuthorization where clients sign off-chain authorizations and the facilitator executes the on-chain transfer.

Supported Networks

  • base-sepolia - Base Sepolia Testnet
  • base - Base Mainnet
  • Additional networks supported via EIP-155 format (e.g., eip155:137 for Polygon)

Payment Mechanism

EIP-3009 Transfer Authorization

EIP-3009 allows users to authorize token transfers via signed messages instead of direct transactions:
  1. Client signs authorization: Creates an off-chain EIP-712 signature authorizing the transfer
  2. Facilitator executes: Calls transferWithAuthorization on the token contract
  3. Facilitator pays gas: The facilitator’s wallet pays transaction fees
  4. Transfer executes: Token contract validates signature and executes transfer
This approach allows gasless payments for clients - they only need tokens, no ETH for gas.

EIP-712 Typed Data

The authorization uses EIP-712 structured data signing for security:
const domain = {
  name: "USDC",
  version: "2",
  chainId: 84532,
  verifyingContract: "0x..."
};

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: clientAddress,
  to: merchantAddress,
  value: BigInt(amountInSmallestUnits),
  validAfter: BigInt(Math.floor(Date.now() / 1000)),
  validBefore: BigInt(Math.floor(Date.now() / 1000) + 3600),
  nonce: randomBytes32()
};

const signature = await wallet.signTypedData(domain, types, message);

Requirements Enrichment

When a resource server calls /accepts, the EVM handler adds these fields to the extra object:
name
string
required
EIP-712 domain name (token name).Example: "USDC"
version
string
required
EIP-712 domain version.Example: "2"
chainId
number
required
Chain identifier for the network.Example: 84532 for Base Sepolia
verifyingContract
string
required
Token contract address (or forwarder contract address if using EIP-3009 forwarding) for EIP-712 verification.Example: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" (USDC on Base Sepolia)

Settlement Validation

When processing a /settle request, the EVM handler performs these checks:

1. Signature Verification

Verifies the EIP-712 signature matches the authorization:
import { verifyTypedData } from 'viem';

const isValidSignature = await verifyTypedData({
  address: payload.from,
  domain,
  types,
  primaryType: 'TransferWithAuthorization',
  message: {
    from: payload.from,
    to: payload.to,
    value: payload.value,
    validAfter: payload.validAfter,
    validBefore: payload.validBefore,
    nonce: payload.nonce
  },
  signature
});

if (!isValidSignature) {
  throw new Error("Invalid signature");
}

2. Amount Validation

Ensures the authorized amount matches requirements:
if (BigInt(payload.value) !== BigInt(requirements.maxAmountRequired)) {
  throw new Error("Invalid transfer amount");
}

3. Recipient Validation

Verifies the recipient is the merchant:
if (payload.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
  throw new Error("Invalid recipient address");
}

4. Time Validity

Checks authorization is within its valid time window:
const now = Math.floor(Date.now() / 1000);

if (now < Number(payload.validAfter)) {
  throw new Error("Authorization not yet valid");
}

if (now > Number(payload.validBefore)) {
  throw new Error("Authorization expired");
}

5. Nonce Check

Verifies the nonce hasn’t been used (on-chain check):
const authorizationState = await tokenContract.authorizationState(
  payload.from,
  payload.nonce
);

if (authorizationState) {
  throw new Error("Nonce already used");
}

Transaction Execution

After validation, the facilitator:
  1. Calls token contract: Executes transferWithAuthorization
  2. Pays gas: Facilitator wallet covers transaction fees
  3. Waits for confirmation: Polls for transaction receipt
  4. Returns result: Provides transaction hash
const tx = await tokenContract.transferWithAuthorization(
  payload.from,
  payload.to,
  payload.value,
  payload.validAfter,
  payload.validBefore,
  payload.nonce,
  payload.v,
  payload.r,
  payload.s
);

const receipt = await tx.wait();

return {
  success: true,
  txHash: receipt.hash,
  networkId: '84532',
  error: null
};

Token Support

The EVM handler supports EIP-3009 compatible tokens:
  • USDC: Native transferWithAuthorization support on all chains
  • Custom tokens: Any ERC-20 token with EIP-3009 support
Not all ERC-20 tokens support EIP-3009. Check token contract documentation.

Gas Sponsorship

The facilitator wallet must:
  • Hold sufficient native currency (ETH, ETH on Base, etc.) for gas
  • Pay gas for each transferWithAuthorization call
  • Monitor gas prices to avoid excessive costs
Gas costs vary based on network conditions and current gas prices.

Error Scenarios

Common errors during EVM payment processing:
ErrorCauseSolution
Invalid signatureSignature doesn’t match authorization dataVerify EIP-712 domain and message match exactly
Invalid transfer amountValue doesn’t match requirementsCheck amount uses token’s smallest units
Invalid recipient addressto field doesn’t match merchantVerify payTo from requirements
Authorization expiredCurrent time > validBeforeCreate new authorization with fresh timestamp
Nonce already usedNonce was used in previous transactionGenerate new random nonce
Insufficient allowanceToken balance too lowClient needs sufficient token balance