personal_sign or seen wallet signature prompts before.
The canonical spec is EIP-712 on eips.ethereum.org.
Why not personal_sign
personal_sign signs an opaque byte string. That creates two problems:
- Humans can’t audit it. A wallet prompt showing
0x1901af3b...gives the signer no idea whether they’re approving a 1 USDC payment or draining their wallet. This is the classic blind-signing phishing vector. - Machines can’t verify structure. A contract receiving a raw-message signature can check who signed but has no standard way to know what fields were signed, so every protocol would invent its own fragile encoding.
value: 1000000, to: 0x3f2a...), wallets can render it legibly, and contracts can reconstruct the exact hash from the same field values. A signature over one message type can never be replayed as a different type or against a different contract, because the type definitions and the domain are part of the hash.
Domain separator
The domain binds a signature to one contract on one chain. Its standard fields:
The domain separator is
keccak256 of these encoded fields. Because chainId and verifyingContract are inside it, a signature made for USDC on Base Sepolia is worthless on Base mainnet or against any other contract — replay protection by construction.
Types and message structure
A full EIP-712 payload has four parts:types— the struct definitions: each type is a list of{name, type}fields.EIP712Domainis always among them.primaryType— which type themessageis an instance of.domain— values for the domain separator fields above.message— the actual field values being signed.
hashStruct hashes the type definition (typeHash) together with the encoded field values, so changing a field name, type, or order produces a different digest.
Verification via ecrecover
The signature is a standard secp256k1(v, r, s) triple. On-chain, a contract rebuilds the digest from the submitted field values and recovers the signer:
transferWithAuthorization (see ERC-3009): it reconstructs the digest from from, to, value, validAfter, validBefore, nonce, recovers the signer, and requires it to equal from.
SohoPay’s payment signature
When an agent pays an x402 challenge, it signs an ERC-3009 transfer authorization as EIP-712 typed data. The full payload for a 1 USDC payment on Base Sepolia (sandbox):Typed data
An agent’s signature is necessary but not sufficient: under SohoPay’s 2-of-3 MPC scheme, the Policy Service must co-sign before anything settles. In the sandbox you can skip manual signing entirely with
"signature": "sandbox_auto" — see the Quickstart.Next steps
ERC-3009
The transfer authorization message that payment signatures cover.
Key Management
Managing agent keys when you sign payments yourself.
MPC Signing
Why one signature never moves funds on its own.

