Overview
Crossmint smart wallets are ERC-4337 compliant smart contract accounts that work before deployment. They enable autonomous agent payments without private key management.
Key features:
No Private Keys Controlled via API or email OTP—no key files to manage
Pre-Deployment Signing Sign transactions before wallet exists on-chain (ERC-6492)
Auto-Deployment Deploys automatically on first transaction
Smart Contract Signatures Validate signatures via EIP-1271 after deployment
Why Smart Wallets?
Traditional EOA (Externally Owned Account) wallets have major limitations for agents:
Challenge EOA Wallet Smart Wallet Private Key Management ❌ Must store/manage keys ✅ API-based control Deployment Cost ✅ Free (just address) ✅ Free until first use Signature Validation ✅ Standard ECDSA ✅ Custom logic via EIP-1271 Gas Abstraction ❌ Must hold ETH for gas ✅ Sponsor gas or use paymasters Multi-sig ❌ Not supported ✅ Built-in support Recovery ❌ Lose key = lose wallet ✅ Social recovery possible Batch Transactions ❌ One at a time ✅ Multiple ops in one TX
Smart wallets are purpose-built for agents.
Creating a Wallet
Crossmint wallets can be created server-side (API key) or client-side (email OTP):
Server-Side (API Key)
Perfect for backend agents that need autonomous payment capabilities:
import { createCrossmint , CrossmintWallets } from "@crossmint/wallets-sdk" ;
const crossmint = createCrossmint ({
apiKey: process . env . CROSSMINT_API_KEY
});
const crossmintWallets = CrossmintWallets . from ( crossmint );
const wallet = await crossmintWallets . createWallet ({
chain: "base-sepolia" ,
signer: { type: "api-key" },
owner: "userId:my-guest-agent:evm:smart"
});
console . log ( "Wallet address:" , wallet . address );
// 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb
See events-concierge/src/agents/host.ts:245-254
Client-Side (Email OTP)
Perfect for user-controlled wallets with email authentication:
import { CrossmintEmbeddedWallet } from "@crossmint/client-sdk-react-ui" ;
const { login } = useCrossmintWallet ();
// Trigger email OTP
await login ({ email: "[email protected] " });
// User enters code, wallet is created
const wallet = await crossmintWallets . getWallet ();
Owner identifiers like userId:my-guest-agent:evm:smart ensure consistent wallet addresses across sessions. Same owner ID = same wallet address.
Wallet Lifecycle
Smart wallets go through three stages:
Stage 1: Pre-Deployed
Wallet has an address but doesn’t exist on-chain yet:
const wallet = await crossmintWallets . createWallet ({
chain: "base-sepolia" ,
signer: { type: "api-key" }
});
console . log ( "Address:" , wallet . address );
// 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb
// Check deployment status
const isDeployed = await checkWalletDeployment (
wallet . address ,
"base-sepolia"
);
console . log ( "Deployed:" , isDeployed ); // false
Can do:
Generate signatures
Sign EIP-712 typed data
Sign payment messages
Cannot do:
Receive USDC or other tokens
Make on-chain transactions
Stage 2: Deploying
First payment triggers automatic deployment:
// Guest signs payment (pre-deployed wallet)
const signature = await wallet . signTypedData ({
domain: { chainId: 84532 , ... },
types: { Payment: [ ... ] },
message: {
amount: "50000" ,
to: "0xHostWallet" ,
currency: "0xUSDC"
}
});
// Facilitator deploys wallet + settles payment
const tx = await facilitator . settle ( signature );
console . log ( "Deployment TX:" , tx . hash );
// 0xabc...def
What happens:
Facilitator extracts deployment bytecode from ERC-6492 signature
Deploys wallet contract to computed address
Executes USDC transfer in same transaction
Wallet is now on-chain
Stage 3: Deployed
Wallet exists on-chain as a smart contract:
const isDeployed = await checkWalletDeployment (
wallet . address ,
"base-sepolia"
);
console . log ( "Deployed:" , isDeployed ); // true
// Can now receive tokens
await usdcContract . transfer ( wallet . address , "1000000" );
// Signatures validated via EIP-1271
const signature = await wallet . signMessage ( "Hello" );
const valid = await wallet . isValidSignature ( "Hello" , signature );
Benefits:
Receive USDC and other tokens
Execute arbitrary transactions
Gasless operations (with paymasters)
Batch multiple operations
ERC-6492: Pre-Deployment Signatures
ERC-6492 allows wallets to sign messages before deployment by wrapping signatures with deployment bytecode.
How It Works
// 1. Wallet signs message (pre-deployed)
const baseSignature = await wallet . signTypedData ( paymentMessage );
// 2. Crossmint wraps with deployment data
const erc6492Signature = [
factoryAddress , // Contract factory
factoryCalldata , // Deployment parameters
baseSignature // Actual signature
]. encode ();
// 3. Add ERC-6492 magic suffix
const finalSignature = erc6492Signature + "6492649264926492..." ;
Verification Process
Verifiers simulate deployment, then check signature:
// Verifier receives ERC-6492 signature
function verifyERC6492 ( signature , message , expectedSigner ) {
// 1. Check for magic suffix
if ( signature . endsWith ( "6492649264926492..." )) {
// 2. Extract deployment data
const { factory , calldata , innerSig } = decode ( signature );
// 3. Simulate deployment
const simulatedAddress = computeAddress ( factory , calldata );
// 4. Check address matches
if ( simulatedAddress !== expectedSigner ) {
throw new Error ( "Address mismatch" );
}
// 5. Verify signature against simulated contract
return verifySmartContractSignature (
simulatedAddress ,
innerSig ,
message
);
}
// Standard ECDSA verification
return verifyECDSA ( signature , message , expectedSigner );
}
Key insight: The wallet’s address is deterministic (based on factory + parameters), so verifiers can compute it without deployment.
See x402Adapter.ts:79-111
EIP-1271: Smart Contract Signatures
After deployment, wallets use EIP-1271 for signature validation:
The Interface
Smart contract wallets implement isValidSignature():
interface IERC1271 {
/**
* @dev Should return whether the signature provided is valid for the provided data
* @param hash Hash of the data to be signed
* @param signature Signature byte array associated with _data
*/
function isValidSignature (
bytes32 hash ,
bytes memory signature
) external view returns ( bytes4 magicValue );
}
Magic value: 0x1626ba7e means signature is valid.
Verification Flow
import { verifyTypedData } from "viem" ;
// For deployed smart wallets
const valid = await publicClient . call ({
address: wallet . address ,
abi: ERC1271_ABI ,
functionName: "isValidSignature" ,
args: [ messageHash , signature ]
});
if ( valid === "0x1626ba7e" ) {
console . log ( "✅ Signature valid" );
} else {
console . log ( "❌ Signature invalid" );
}
Custom Validation Logic
Smart wallets can implement custom signature validation:
contract CrossmintSmartWallet {
mapping ( address => bool ) public owners;
function isValidSignature (
bytes32 hash ,
bytes memory signature
) external view returns ( bytes4 ) {
// Recover signer from signature
address signer = recoverSigner (hash, signature);
// Check if signer is an owner
if (owners[signer]) {
return 0x1626ba7e ; // Valid
}
return 0xffffffff ; // Invalid
}
}
Possibilities:
Multi-sig (require N of M owners)
Time-locked signatures
Spending limits
Session keys
Social recovery
x402 Integration
Crossmint wallets integrate seamlessly with x402 via an adapter:
import { Wallet , EVMWallet } from "@crossmint/wallets-sdk" ;
import type { Hex } from "viem" ;
/**
* Create an x402-compatible signer from Crossmint wallet
*/
export function createX402Signer ( wallet : Wallet < any >) {
const evm = EVMWallet . from ( wallet );
return {
address: evm . address as `0x ${ string } ` ,
type: "local" ,
// x402 calls this to sign payments
signTypedData : async ( params : any ) => {
const { domain , message , primaryType , types } = params ;
// Sign with Crossmint
const sig = await evm . signTypedData ({
domain ,
message ,
primaryType ,
types ,
chain: evm . chain
});
// Process signature (handles ERC-6492)
return processSignature ( sig . signature );
}
};
}
function processSignature ( rawSignature : string ) : Hex {
const signature = rawSignature . startsWith ( '0x' )
? rawSignature
: `0x ${ rawSignature } ` ;
// ERC-6492 (pre-deployed) - keep as-is
if ( signature . endsWith ( "6492649264926492649264926492649264926492649264926492649264926492" )) {
return signature as Hex ;
}
// EIP-1271 (deployed) - 174 chars
if ( signature . length === 174 ) {
return signature as Hex ;
}
// Standard ECDSA - 132 chars
if ( signature . length === 132 ) {
return signature as Hex ;
}
// Fallback: extract standard signature
return ( '0x' + signature . slice ( - 130 )) as Hex ;
}
See x402Adapter.ts
Usage:
const crossmintWallet = await crossmintWallets . createWallet ({ ... });
const x402Signer = createX402Signer ( crossmintWallet );
const server = new McpServer ({ name: "Event Host" })
. withX402 ({
wallet: x402Signer , // Works with x402!
network: "base-sepolia" ,
recipient: hostAddress
});
Checking Deployment Status
You can check if a wallet is deployed:
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" )
});
const code = await publicClient . getCode ({
address: walletAddress as `0x ${ string } `
});
// If bytecode exists, wallet is deployed
return code !== undefined && code !== '0x' && code . length > 2 ;
}
Usage:
const deployed = await checkWalletDeployment (
wallet . address ,
"base-sepolia"
);
if ( deployed ) {
console . log ( "✅ Wallet deployed, can receive tokens" );
} else {
console . log ( "⚠️ Wallet not deployed yet" );
console . log ( "💡 First payment will deploy it automatically" );
}
See x402Adapter.ts:131-154
Manual Deployment
You can deploy a wallet manually before first payment:
export async function deployWallet ( wallet : Wallet < any >) : Promise < string > {
console . log ( "🚀 Deploying wallet on-chain..." );
const evmWallet = EVMWallet . from ( wallet );
// Deploy with minimal self-transfer (1 wei)
const tx = await evmWallet . sendTransaction ({
to: wallet . address ,
value: 1 n , // 1 wei
data: "0x"
});
console . log ( `✅ Wallet deployed! TX: ${ tx . hash } ` );
return tx . hash ;
}
When to use:
You want to pre-deploy before accepting payments
You need to receive tokens before first payment
You’re testing wallet functionality
Deployment requires gas: The wallet needs a small amount of ETH for gas fees. Use the Base Sepolia faucet for testnet.
See x402Adapter.ts:159-185
Security Best Practices
Never commit Crossmint API keys to version control: # .env
CROSSMINT_API_KEY = sk_production_...
Use environment variables in production: const crossmint = createCrossmint ({
apiKey: process . env . CROSSMINT_API_KEY
});
Verify Signatures Server-Side
Always verify signatures on the server, never trust client claims: // ❌ DON'T trust client
if ( request . body . signatureValid ) {
executePayment ();
}
// ✅ DO verify server-side
const valid = await verifySignature ( signature , message );
if ( valid ) {
executePayment ();
}
Owner identifiers should be unique per user/agent: // ❌ BAD: Same wallet for all users
owner : "userId:guest-agent"
// ✅ GOOD: Unique per user
owner : `userId:guest-agent- ${ userId } :evm:smart`
Monitor Deployment Status
Check deployment status before critical operations: const deployed = await checkWalletDeployment ( wallet . address );
if ( ! deployed && needsToReceiveTokens ) {
throw new Error ( "Wallet must be deployed first" );
}
Always test with Base Sepolia before mainnet:
Free testnet USDC from Circle faucet
No real money at risk
Same APIs and behavior as mainnet
const chain = process . env . NODE_ENV === "production"
? "base"
: "base-sepolia" ;
Common Issues
Signature Verification Failed
Symptom: “Invalid signature” errorsCauses:
Wrong chain ID in signature domain
Incorrect message structure
Using deployed wallet signature for pre-deployed wallet
Fix: // Ensure chain ID matches
const domain = {
chainId: 84532 , // Must match network
verifyingContract: USDC_ADDRESS
};
Symptom: USDC transfers fail or don’t appearCause: Wallet not deployed yetFix: const deployed = await checkWalletDeployment ( wallet . address );
if ( ! deployed ) {
console . log ( "Deploying wallet first..." );
await deployWallet ( wallet );
}
Insufficient Gas for Deployment
Symptom: “Insufficient funds” error on deploymentCause: Wallet has no ETH for gasFix: # Get testnet ETH from Base Sepolia faucet
# https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet
Wallet Address Keeps Changing
Symptom: New address on each createWallet() callCause: Different owner IDs or missing owner parameterFix: // Use consistent owner ID
const wallet = await crossmintWallets . createWallet ({
chain: "base-sepolia" ,
signer: { type: "api-key" },
owner: "userId:my-agent:evm:smart" // Same every time
});
Next Steps
x402 Protocol Learn how payments work over HTTP
A2A Payments Build agent-to-agent payment flows
Payment Flow See detailed end-to-end flows
Quickstart Create your first smart wallet
Resources