Every signature in SohoPay — payment authorizations, settlement approvals — is an EIP-712 typed-data signature. EIP-712 defines how to hash and sign structured data so both humans and contracts can see exactly what is being signed. This page explains the standard and shows the exact typed data an agent signs for a payment. It assumes you’ve used 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.
EIP-712 fixes both: data is signed as named, typed fields (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. EIP712Domain is always among them.
  • primaryType — which type the message is an instance of.
  • domain — values for the domain separator fields above.
  • message — the actual field values being signed.
The signing hash is:
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:
This is exactly what USDC does inside 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
Signing it in TypeScript:
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.