Skip to main content

Overview

ERC-6492 enables signature verification for smart contract wallets that haven’t been deployed yet. This is critical for Crossmint Agentic Finance because:
  1. Wallet creation is free - Users get an address immediately via API
  2. No deployment costs upfront - Wallets deploy only when first transacting
  3. Signatures work pre-deployment - ERC-6492 wraps signatures with deployment data
Without ERC-6492, users would need to deploy their wallet (pay gas fees) before making their first payment. With ERC-6492, they can sign payments immediately.

How ERC-6492 Works

Standard Wallet Lifecycle

Signature Format Comparison

Wallet StateSignature FormatVerification MethodSignature Length
Pre-DeployedERC-6492 wrappedSimulate deployment, verify signatureVariable (>132 chars)
DeployedStandard ECDSA or EIP-1271Call isValidSignature() on contract132 chars (65 bytes)

ERC-6492 Signature Structure

An ERC-6492 signature wraps the standard signature with deployment information:
+-----------------------------------+
| Standard Signature (65 bytes)     |  ← The actual ECDSA signature
+-----------------------------------+
| Factory Address (20 bytes)        |  ← Address of wallet factory contract
+-----------------------------------+
| Factory Calldata (variable)       |  ← createWallet(owner, salt, ...)
+-----------------------------------+
| Magic Suffix (32 bytes)           |  ← 0x6492649264926492...6492
+-----------------------------------+

Magic Bytes Identifier

ERC-6492 signatures end with a 32-byte magic suffix:
6492649264926492649264926492649264926492649264926492649264926492
This allows verifiers to detect ERC-6492 signatures:
x402Adapter.ts
function isERC6492Signature(signature: string): boolean {
  return signature.endsWith(
    "6492649264926492649264926492649264926492649264926492649264926492"
  );
}

Signature Processing

The x402 adapter detects and handles different signature formats:
x402Adapter.ts
function processSignature(rawSignature: string): Hex {
  const signature = ensureHexPrefix(rawSignature);

  console.log(`📝 Processing signature: ${signature.substring(0, 20)}... (${signature.length} chars)`);

  // Handle ERC-6492 wrapped signatures (pre-deployed wallets)
  if (isERC6492Signature(signature)) {
    console.log("✅ ERC-6492 signature detected - keeping for facilitator");
    return signature;  // Keep full wrapped signature
  }

  // Handle EIP-1271 signatures (deployed smart contract wallets)
  if (signature.length === 174) {  // 87 bytes * 2
    console.log("✅ EIP-1271 signature detected");
    return signature;
  }

  // Handle standard ECDSA signatures (65 bytes / 132 hex chars)
  if (signature.length === 132) {
    console.log("✅ Standard ECDSA signature");
    return signature;
  }

  // Handle non-standard lengths - extract standard signature
  if (signature.length > 132) {
    const extracted = '0x' + signature.slice(-130);
    console.log(`🔧 Extracted standard signature from longer format`);
    return extracted as Hex;
  }

  console.log("⚠️ Using signature as-is");
  return signature;
}
Key insight: ERC-6492 signatures are much longer than standard signatures because they include deployment bytecode.

Verification Process

The x402 facilitator verifies ERC-6492 signatures in two steps:

Step 1: Check Wallet Deployment

import { createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";

export async function checkWalletDeployment(
  walletAddress: string,
  chain: string
): Promise<boolean> {
  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http("https://sepolia.base.org")
  });

  // Check if bytecode exists at wallet address
  const code = await publicClient.getCode({
    address: walletAddress as `0x${string}`
  });

  // If code exists and is not just "0x", wallet is deployed
  return code !== undefined && code !== '0x' && code.length > 2;
}

Step 2: Verify Signature

If wallet is deployed (code exists):
// Call EIP-1271 isValidSignature on the deployed contract
bytes4 magicValue = wallet.isValidSignature(messageHash, signature);
require(magicValue == 0x1626ba7e, "Invalid signature");
If wallet is NOT deployed (no code):
// Extract deployment data from ERC-6492 signature
(address factory, bytes memory factoryCalldata, bytes memory actualSignature) = decodeERC6492(signature);

// Simulate deployment in a test environment
address predictedAddress = simulateDeployment(factory, factoryCalldata);
require(predictedAddress == expectedWalletAddress, "Deployment simulation failed");

// Verify signature against the simulated contract
verifySignature(predictedAddress, messageHash, actualSignature);

Crossmint Wallet Deployment

Crossmint wallets deploy automatically on first transaction. However, for x402 payments, you may need to deploy manually before settlement:
x402Adapter.ts
export async function deployWallet(wallet: Wallet<any>): Promise<string> {
  console.log("🚀 Deploying wallet on-chain...");

  const evmWallet = EVMWallet.from(wallet);

  // Deploy wallet with a minimal self-transfer (1 wei)
  const deploymentTx = await evmWallet.sendTransaction({
    to: wallet.address,
    value: 1n,  // 1 wei triggers deployment
    data: "0x"
  });

  console.log(`✅ Wallet deployed! Transaction: ${deploymentTx.hash}`);
  return deploymentTx.hash;
}
Why self-transfer?
  • Minimal gas cost (just deployment, no meaningful transfer)
  • Simple transaction (no complex logic)
  • Guaranteed to succeed (wallet sends to itself)

Payment Flow with Pre-Deployed Wallet

Guest Agent Implementation

From src/agents/guest.ts:384-431:
case "confirm": {
  const confirmed = parsed.type === "confirm";

  if (confirmed && this.wallet) {
    // Check wallet deployment status
    const isDeployed = await checkWalletDeployment(
      this.wallet.address,
      "base-sepolia"
    );

    if (!isDeployed) {
      this.broadcastLog('payment', `⚠️ Wallet is pre-deployed (ERC-6492 mode)`);
      this.broadcastLog('payment', `🚀 Deploying wallet on-chain for settlement...`);

      // Deploy wallet before payment settlement
      const deploymentTxHash = await deployWallet(this.wallet);

      this.broadcastLog('system', `✅ Wallet deployed successfully!`);
      this.broadcastLog('system', `📝 Deployment tx: ${deploymentTxHash}`);

      // Broadcast updated wallet info
      this.broadcast(
        JSON.stringify({
          type: "wallet_info",
          guestAddress: this.wallet.address,
          hostAddress: this.hostWalletAddress,
          network: "base-sepolia",
          guestWalletDeployed: true
        })
      );
    } else {
      this.broadcastLog('payment', `✅ Wallet already deployed`);
    }
  }

  // Confirm payment (signature will be ERC-6492 if still pre-deployed)
  this.confirmations[parsed.confirmationId]?.(confirmed);
  break;
}

When to Deploy vs Use ERC-6492

Use ERC-6492

Best for: First-time payments, testing, low gas environments
  • No upfront deployment cost
  • Signature works immediately
  • Facilitator handles deployment during settlement

Deploy First

Best for: Production, high-value payments, known repeat usage
  • Faster payment settlement
  • Standard EIP-1271 verification
  • Avoid deployment delay in critical flows

Error Handling

try {
  await deployWallet(wallet);
} catch (error) {
  if (error.message.includes('insufficient') || error.message.includes('balance')) {
    throw new Error("Insufficient ETH balance for deployment gas fees");
  }
  throw error;
}
Solution: Fund the wallet with ETH for gas before deploying.
if (!isERC6492Signature(signature) && !isDeployed) {
  throw new Error("Wallet not deployed but signature is not ERC-6492 format");
}
Solution: Ensure Crossmint SDK is returning proper ERC-6492 signatures for pre-deployed wallets.
// Facilitator checks predicted address matches expected
if (predictedAddress !== expectedWalletAddress) {
  throw new Error("ERC-6492 deployment simulation mismatch");
}
Solution: Verify factory address and calldata in the ERC-6492 signature are correct.

Testing Pre-Deployment Signatures

// 1. Create wallet (pre-deployed)
const wallet = await crossmintWallets.createWallet({
  chain: "base-sepolia",
  signer: { type: "api-key" },
  owner: "userId:test-user"
});

console.log("Wallet address:", wallet.address);

// 2. Check deployment status (should be false)
const isDeployed = await checkWalletDeployment(wallet.address, "base-sepolia");
console.log("Deployed?", isDeployed);  // false

// 3. Sign payment (will return ERC-6492 signature)
const evmWallet = EVMWallet.from(wallet);
const signature = await evmWallet.signTypedData({
  domain: { name: "x402 Payment", version: "1", chainId: 84532 },
  types: { Payment: [/* ... */] },
  primaryType: "Payment",
  message: { amount: "50000", to: "0x...", /* ... */ },
  chain: "base-sepolia"
});

console.log("Signature format:", isERC6492Signature(signature.signature) ? "ERC-6492" : "Standard");
// Output: "ERC-6492"

// 4. Deploy wallet
const txHash = await deployWallet(wallet);
console.log("Deployed! TX:", txHash);

// 5. Sign again (now will return standard signature)
const signature2 = await evmWallet.signTypedData(/* same payload */);
console.log("Signature format:", isERC6492Signature(signature2.signature) ? "ERC-6492" : "Standard");
// Output: "Standard"

Implementation Checklist

1

Detect ERC-6492 Signatures

Check for the magic suffix when processing signatures.
2

Verify Deployment Status

Use getCode() to check if wallet is deployed before payment.
3

Handle Pre-Deployed Wallets

Pass full ERC-6492 signature to facilitator for verification.
4

Deploy When Needed

Trigger deployment before settlement if required by your flow.
5

Fallback to EIP-1271

After deployment, signatures automatically use standard EIP-1271 format.

EIP-712 Signatures

Human-readable typed data signing

x402 Facilitator

Payment verification and settlement

References

Build docs developers (and LLMs) love