ARCHITECTURE.md section 16.3 (“x402 signature path”), contracts/src/X402PaymentValidator.sol, THREAT-MODEL.md scenario I and invariant 17.
The problem
An x402 payment on EVM is, by default, an EIP-3009transferWithAuthorization that the account signs and a facilitator submits. For a smart account that signature is checked through ERC-1271, a view call, so the payment never runs through the account’s execution and never reaches the spend policy’s record. Two facts follow, both checked against Kernel v3.1’s source and reproduced on a Monad mainnet fork on 2026-10-10:
- A policy on Kernel v3.1 sees only a raw 32-byte hash on the signature path, never the typed data, so it cannot decode what is being signed.
- If the spend policy were allowed to sign through ERC-1271, a session key’s signature would make USDC grant a
permitof the account’s whole balance to any spender.
Decision
A dedicated x402 key and a separate Kernel validator module,X402PaymentValidator, which the owner installs beside the trading permission. It approves an ERC-1271 signature only for an EIP-3009 transferWithAuthorization from the account to a payee the owner listed. Kernel’s skip-signature flag is never set on the spend policy, for any agent. The trading key therefore cannot sign for the account at all, and the x402 key can sign nothing but bounded payments. Each key holds exactly one power.
x402 payment flow with the validator's on-chain bounds on the signed authorization: a listed payee, at most 0.05 USDC, validity of at most 300 seconds, and four payments per hour, so at most 5.20 USDC per agent per 24 hours (proposed defaults). The provider serving x402 v2 on Monad and the facilitator accepting ERC-1271 signatures are unverified assumptions. Source: ARCHITECTURE.md section 16.3.
What it enforces
The signature carries the authorization’s fields. The validator succeeds only if all of these hold:- the configuration for the calling account is live;
tois a listed payee;0 < value <= maxValue;0 < validBefore - validAfter <= maxValidity, withmaxValidityat most one hour;slot < paymentsPerEpoch;- the wrapped digest of
TransferWithAuthorization(account, to, value, validAfter, validBefore, nonce), with the nonce derived from the account, the hour ofvalidAfter, and the slot, equals the hash Kernel passed; - the signature is the configured x402 key’s low-s signature of that hash.
permit, a receiveWithAuthorization, another token’s digest, or any other typed data cannot match, whatever fields accompany it. validateUserOp always fails, so the key can never validate an operation.
How the 24-hour bound is derived
The token accepts each nonce once per authorizer, and the nonce is a function of the account, the hour ofvalidAfter, and a slot below paymentsPerEpoch, so each hour admits at most paymentsPerEpoch payments of at most maxValue. A payment executes strictly between its validAfter and validBefore, at most one hour apart, so payments executed in any 24-hour period have validAfter inside a 25-hour interval, which touches at most 26 hours. Therefore at most
dailyCap, so nothing is counted twice. The owner-decided defaults are maxValue 0.05 USDC and 4 payments per hour (a 24-hour worst case of 5.20 USDC per agent), with maxValidity 300 seconds. The application-layer information caps remain the tighter limits.
Why not the alternatives
Revocation and replay
Revoking the x402 key means uninstalling the validator, because Kernel v3.1’s ERC-1271 path does not read the nonce. Uninstalling also invalidates every authorization signed earlier and not yet settled, since USDC checks the signature at settlement. Replay of one authorization is stopped by the token’s nonce registry, across accounts byfrom and Kernel’s account-domain wrapping, and across chains by both domains’ chain id. Once signed, an authorization can be submitted by anyone, but only for its signed payee and amount, which is a property of the standard.