Skip to main content

Overview

EIP-712 (Ethereum Improvement Proposal 712) provides a standard for signing typed structured data. In Crossmint Agentic Finance, EIP-712 signatures enable human-readable payment confirmations where users can verify exactly what they’re signing before authorizing a payment. Unlike raw message signing (which shows cryptic hex strings), EIP-712 presents structured data in a readable format, improving security and user trust.

Why EIP-712 for Payments?

Human Readable

Users see clear payment details: amount, recipient, currency, and chain ID

Type Safety

Structured schemas prevent malformed payment data

Replay Protection

Domain separators and nonces prevent signature reuse

Wallet Compatible

Supported by MetaMask, WalletConnect, and Crossmint wallets

EIP-712 Signature Structure

An EIP-712 signature consists of four components:
const eip712Payload = {
  // Domain separator - prevents cross-chain/cross-contract replay
  domain: {
    name: "x402 Payment",
    version: "1",
    chainId: 84532,  // Base Sepolia
    verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"  // USDC contract
  },

  // Type definitions for the message
  types: {
    Payment: [
      { name: "amount", type: "uint256" },
      { name: "currency", type: "address" },
      { name: "to", type: "address" },
      { name: "nonce", type: "uint256" },
    ]
  },

  // Primary type being signed
  primaryType: "Payment",

  // The actual payment data
  message: {
    amount: "50000",  // 0.05 USDC (6 decimals)
    currency: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",  // USDC on Base Sepolia
    to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",  // Recipient (host wallet)
    nonce: 1709856000000  // Unix timestamp for uniqueness
  }
};

Domain Separator

The domain prevents signature replay attacks across:
  • Different chains: chainId ensures signatures are chain-specific
  • Different protocols: name and version identify the protocol
  • Different contracts: verifyingContract ties signature to a specific contract

Type Definitions

Defines the schema for the payment message:
  • amount (uint256): Payment amount in smallest unit (e.g., 50000 = 0.05 USDC)
  • currency (address): Token contract address
  • to (address): Recipient wallet address
  • nonce (uint256): Unique value to prevent replay attacks

Signing with Crossmint Wallets

The x402 adapter bridges Crossmint wallets to the viem Account interface expected by x402:
x402Adapter.ts
import { Wallet, EVMWallet } from "@crossmint/wallets-sdk";
import type { Hex } from "viem";

export function createX402Signer(wallet: Wallet<any>) {
  const evm = EVMWallet.from(wallet);

  return {
    address: evm.address as `0x${string}`,
    type: "local",
    source: "custom",

    // EIP-712 signing method
    signTypedData: async (params: any) => {
      const { domain, message, primaryType, types } = params;

      console.log("🔐 Signing EIP-712 payment:");
      console.log("  Payer:", evm.address);
      console.log("  Recipient:", message.to);
      console.log("  Amount:", message.amount);
      console.log("  Network:", domain.chainId);

      // Sign with Crossmint wallet
      const sig = await evm.signTypedData({
        domain,
        message,
        primaryType,
        types,
        chain: evm.chain
      });

      return processSignature(sig.signature);
    }
  };
}
Key points:
  • Crossmint wallets support both ERC-6492 (pre-deployed) and EIP-1271 (deployed) signatures
  • The adapter logs payment details for transparency
  • Signature processing handles different formats (see ERC-6492 Validation)

Payment Signature Flow

Example: Guest Agent Signing

From src/agents/guest.ts:279-285:
// Create x402-compatible signer from Crossmint wallet
const x402Signer = createX402Signer(this.wallet);

// Wrap MCP client with x402 payment support
this.x402Client = withX402Client(this.mcp.mcpConnections[id].client, {
  network: "base-sepolia",
  account: x402Signer  // Will call signTypedData when payment required
});
When a paid tool returns 402, the x402 client automatically:
  1. Extracts payment requirements from the 402 response
  2. Constructs the EIP-712 payload
  3. Calls x402Signer.signTypedData(payload)
  4. Sends signature to the facilitator for verification

Signature Format Output

After signing, signatures are processed based on wallet deployment status:

Pre-Deployed Wallets (ERC-6492)

// Signature ends with ERC-6492 magic bytes
const signature = "0x...6492649264926492649264926492649264926492649264926492649264926492";

if (isERC6492Signature(signature)) {
  console.log("✅ ERC-6492 signature detected - keeping for facilitator");
  return signature;  // Keep full wrapped signature
}
ERC-6492 signatures include:
  • The actual signature bytes
  • Wallet factory address
  • Deployment bytecode
  • Constructor arguments
  • Magic suffix: 0x6492...6492 (32 bytes)
See ERC-6492 Validation for details.

Deployed Wallets (EIP-1271)

// Standard 65-byte ECDSA signature for deployed contracts
if (signature.length === 132) {  // 65 bytes * 2 + "0x"
  console.log("✅ Standard ECDSA signature");
  return signature;
}
EIP-1271 signatures are verified by calling the smart contract’s isValidSignature() function.

Security Considerations

Use unique nonces (timestamps or UUIDs) to prevent replay attacks:
message: {
  amount: "50000",
  currency: USDC_ADDRESS,
  to: hostWallet,
  nonce: Date.now()  // Unique per payment
}
Never reuse nonces - each payment must have a unique identifier.
Always verify the domain matches your application:
// Verifier checks domain before accepting signature
if (domain.chainId !== 84532) {
  throw new Error("Wrong chain - expected Base Sepolia");
}
if (domain.name !== "x402 Payment") {
  throw new Error("Wrong protocol");
}
Verify payment amounts match tool requirements:
// Host checks signed amount matches tool price
const toolPrice = 0.05 * 1_000_000;  // 50000 (6 decimals)
if (BigInt(signedMessage.amount) < BigInt(toolPrice)) {
  throw new Error("Insufficient payment");
}

Implementation Checklist

1

Create x402 Signer

Wrap your Crossmint wallet with createX402Signer() to provide EIP-712 signing capability.
2

Define Payment Schema

Use the standard x402 Payment type with amount, currency, to, and nonce fields.
3

Handle User Confirmation

Show payment details to the user before calling signTypedData().
4

Process Signatures

Handle both ERC-6492 (pre-deployed) and standard ECDSA signatures.
5

Submit to Facilitator

Send the signature in the X-PAYMENT header when retrying the tool call.

ERC-6492 Validation

Pre-deployment signature verification

x402 Facilitator

Payment settlement and verification

References

Build docs developers (and LLMs) love