EIP-3009 and EIP-712 in x402 payments

On EVM chains, the x402 exact scheme uses EIP-3009 authorizations by default: the payer signs an EIP-712 message off chain and the facilitator submits it. This guide explains every signed field, how the x402 exact scheme fills them in, and what a signature does and does not prove.

In short

By default, x402's exact scheme on EVM chains has the payer sign an EIP-3009 transferWithAuthorization with EIP-712: from, to, value, a validity window and a random nonce, bound to one token and chain. The facilitator submits it and cannot change amount or recipient; balance and nonce use need chain state.

On this page

EIP-3009, "Transfer With Authorization", is a draft ERC created in 2020. It adds functions to an ERC-20 token so a holder can authorize a transfer by signing a typed message, and anyone holding that signature can submit it on chain. The signer never sends a transaction or pays gas. The signature itself follows EIP-712, the standard for hashing and signing typed structured data.

In x402, EIP-3009 is the default asset transfer method of the exact scheme on EVM chains, recommended for tokens that implement transferWithAuthorization natively, with USDC as the specification's example. Tokens without it use Permit2, and smart accounts can use ERC-7710 delegation; both have a different payload and are out of scope here.

What transferWithAuthorization does

The token contract exposes this function. It checks the authorization and the signature, marks the nonce as used and moves value from from to to:

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

function authorizationState(address authorizer, bytes32 nonce)
    external view returns (bool);

event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);

The EIP also defines receiveWithAuthorization, which additionally requires the caller to be the payee, and an optional cancelAuthorization that lets the payer cancel an authorization before it is used.

The authorization fields

These six values are what the payer signs. In an x402 payload they travel as strings in payload.authorization, next to the 65-byte signature.

TransferWithAuthorization fields and what the EIP-3009 reference implementation enforces
FieldTypeMeaningEnforced on chain
fromaddressThe payer, who signs.The recovered signer must equal from.
toaddressThe payee.Funds go here and nowhere else.
valueuint256Amount in the token's atomic units.Exactly this amount is transferred.
validAfteruint256Unix time after which the authorization is valid.now > validAfter
validBeforeuint256Unix time before which it is valid.now < validBefore
noncebytes32A random 32-byte value chosen by the payer.Must be unused for this payer, then marked used.

The nonces are random rather than sequential, one of the two differences the EIP lists against EIP-2612 permits; the other is that tokens move directly, without the ERC-20 allowance step. With sequential nonces, pending authorizations fail when they are submitted out of order; random nonces let a payer create many authorizations in parallel without that risk. Anyone can ask authorizationState(from, nonce) whether a nonce has been used.

The EIP-712 domain

EIP-712 signs a hash of typed data inside a domain, so a signature made for one contract on one chain cannot be replayed elsewhere. EIP-3009 uses four domain fields:

  • name: the human-readable name of the signing domain, here the token's EIP-712 name.
  • version: the current major version of the domain. Signatures from different versions are not compatible.
  • chainId: the EIP-155 chain id.
  • verifyingContract: the contract that verifies the signature, here the token itself.

The digest the payer signs combines the domain separator with the hash of the message:

TextEIP-712 digest for TransferWithAuthorization
domainSeparator = keccak256(abi.encode(
    keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
    keccak256(name), keccak256(version), chainId, verifyingContract))

structHash = keccak256(abi.encode(
    TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
    from, to, value, validAfter, validBefore, nonce))

digest = keccak256(0x1901 ‖ domainSeparator ‖ structHash)

TRANSFER_WITH_AUTHORIZATION_TYPEHASH =
  keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
  = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267

Wallets produce this signature with eth_signTypedData_v4, given the domain, the type definitions and the message.

How the x402 exact EVM scheme uses it

An x402 payment requirement carries everything needed to rebuild the domain, and the payload carries the message. The facilitator, which submits the transaction and pays the gas, cannot change the amount or the destination: both are inside the signed message.

Where each signed value comes from in an x402 exact EVM payment.
x402 requirement fields and their role in the EIP-3009 signature
x402 fieldRole in the signature
extra.nameSigned as the domain name. Required for EIP-3009: a wrong name produces a signature the token rejects.
extra.versionSigned as the domain version. Required for EIP-3009, for the same reason.
networkIts reference is the domain chainId: eip155:84532 signs chain 84532. Binds the signature to one chain.
assetSigned as the domain verifyingContract. Binds the signature to one token contract.
payToMust equal the message's to, or the payment goes elsewhere.
amountMust equal the message's value. The core specification lists invalid_exact_evm_payload_authorization_value_mismatch for this case (see the x402 error codes).
maxTimeoutSecondsNot signed. It is the maximum time allowed for payment completion; keep the authorization window short as well.

According to the exact EVM specification, a facilitator verifying an EIP-3009 payment checks that the signature recovers to authorization.from, that the payer has enough balance, that the amount and validity window meet the requirement, that the token and network match, and then simulates transferWithAuthorization. Settlement calls that function with the signature and the authorization. The core specification adds that EIP-3009 contracts prevent nonce reuse on chain, which is what stops a replayed authorization.

Worked example: the specification's own payment

The PAYMENT-SIGNATURE example in the x402 V2 HTTP transport specification is a real signature. Settlens' verifier, run when this page was built, rebuilds the domain from the example requirement and recovers the payer:

Values computed at build time from the specification example
ValueResult
Domainname "USDC", version "2", chainId 84532, verifyingContract 0x036CbD53842c5426634e7929541eC2318f3dCF7e
Domain separator0x71f17a3b2ff373b803d70a5a07c046c1a2bc8e89c09ef722fcb047abe94c9818
Struct hash0x49ad644555c7bdf3ea4fc022a4940ae5589b230b1dde0065c5504fa19bbe39a4
Digest0xf256992871671abcb27ff92885a7afa46218724e5fc0bac35d050115aa1d22e6
Recovered signer0x857b06519e91e3a54538791bdbb0e22373e36b66, equal to authorization.from

The authorization was valid for 65 seconds on February 27, 2025, between validAfter 1740672089 and validBefore 1740672154. Checked at a time inside that window, for example with the MCP tool verify_payment_signature and its now argument, every property holds:

JSONverify_payment_signature result with now = 1740672100
{
  "scheme": "exact",
  "recovered": "0x857b06519e91e3a54538791bdbb0e22373e36b66",
  "signatureValid": true,
  "recipientBound": true,
  "amountBound": true,
  "fresh": true,
  "verified": true,
  "reasons": [],
  "checkedAt": 1740672100
}

Checked with the CLI at a time on October 13, 2026 (--now; without it the CLI uses its clock), long after the window closed, the same file is rejected for that reason alone:

settlens verify-payment (window expired)
settlens verify-payment payment.json --now 2026-10-13T10:00:00Z
REJECTED exact EVM payment authorization
Recovered signer: 0x857b06519e91e3a54538791bdbb0e22373e36b66
Signature valid:  true
Recipient bound:  true
Amount bound:     true
Within window:    false (at 1791885600)
Reasons:
  - authorization is outside its validAfter/validBefore window (EIP-3009 requires validAfter < now < validBefore).

Change only the network in the requirement to Base mainnet (eip155:8453) and the rebuilt domain has a different chainId. The digest changes, so the signature recovers a different, meaningless address. This is the cross-chain replay protection EIP-712 provides:

settlens verify-payment (same signature, other chain)
settlens verify-payment payment-base-mainnet.json --now 2026-10-13T10:00:00Z
REJECTED exact EVM payment authorization
Recovered signer: 0x46e5af7a18131f1bc0947cf44151ee84b58c92a9
Signature valid:  false
Recipient bound:  true
Amount bound:     true
Within window:    false (at 1791885600)
Reasons:
  - Recovered signer 0x46e5af7a18131f1bc0947cf44151ee84b58c92a9 does not match authorization.from 0x857b06519E91e3A54538791bDbb0E22373e36b66.
  - authorization is outside its validAfter/validBefore window (EIP-3009 requires validAfter < now < validBefore).

What can be verified offline, and what needs the chain

A signature check proves that the payer agreed to specific terms. It does not prove the payment will settle. The split matters when you decide what an offline verification, such as Settlens' verify-payment, can tell you.

Properties of an EIP-3009 payment authorization by where they can be checked
PropertyOffline, no RPCNeeds chain state
Signature recovers to authorization.from
to equals the advertised payTo
value equals the advertised amount
Bound to the advertised token and chainYes, through the domain
Inside validAfter and validBeforeYes, against a clock
extra.name and extra.version are the token's real domain
Payer balance covers value
Nonce not used yet (authorizationState)
Token implements transferWithAuthorization
Transfer settled

Settlens' verify-payment covers the first five rows and stops there by design: it needs no RPC, wallet or key, and it never checks balances, nonce use or settlement. Read more on the Verify product page or in the CLI reference. To check one payment without installing anything, use the EIP-3009 signature verifier.

Security notes for implementers

  • Reject the zero address. ecrecover returns the zero address for a malformed signature; EIP-3009 requires rejecting it, otherwise funds of the zero address could be "authorized".
  • Prefer canonical signatures. EIP-2 made transaction signatures with an s value above secp256k1n/2 invalid, but the ecrecover precompile still accepts them, so each signature has a malleable twin. Settlens rejects high-s signatures.
  • Use short windows and random nonces. Anyone who obtains a signed authorization can submit it until validBefore, even if the resource was never delivered. Generate each nonce randomly.
  • Check the domain hints. An offline check compares the signature with the domain the server advertised. If extra.name or extra.version are wrong, the signature verifies offline and still fails on chain.
  • Contracts should receive, not transfer. EIP-3009 warns that a contract wrapping transferWithAuthorization can be front-run from the mempool, and recommends receiveWithAuthorization, which checks that the caller is the payee.

Checklist

These points, and the server and facilitator behaviors around them, are part of the x402 security checklist.

Frequently asked questions

How is EIP-3009 different from an EIP-2612 permit?

An EIP-2612 permit approves an allowance with a sequential nonce, and a second transaction spends it with transferFrom. EIP-3009 moves the tokens directly in one call and uses random 32-byte nonces, so several authorizations can be pending at once.

Can the facilitator change the amount or the recipient?

No. to and value are part of the signed message. Changing either changes the digest, so the signature no longer recovers to the payer and the token rejects the transfer.

Does a valid signature mean the payment will settle?

No. Settlement also needs enough balance, an unused nonce, a token that implements EIP-3009 with the advertised domain, and a successful transaction. Those depend on chain state.

Why does x402 put the token name and version in extra?

They are part of the EIP-712 domain. Without them the client cannot build the digest to sign, and a verifier cannot rebuild it to check the signature without calling the chain.

Sources

  1. ERC-3009: Transfer With AuthorizationEthereum Improvement Proposals · eips.ethereum.org
  2. EIP-712: Typed structured data hashing and signingEthereum Improvement Proposals · eips.ethereum.org
  3. EIP-155: Simple replay attack protectionEthereum Improvement Proposals · eips.ethereum.org
  4. EIP-2: Homestead Hard-fork ChangesEthereum Improvement Proposals · eips.ethereum.org
  5. ERC-2612: Permit Extension for EIP-20 Signed ApprovalsEthereum Improvement Proposals · eips.ethereum.org
  6. x402 scheme: exact on EVMx402 Foundation · github.com
  7. x402 protocol specification, version 2x402 Foundation · github.com