Documentation Index Fetch the complete documentation index at: https://mintlify.com/Crossmint/crossmint-agentic-finance/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This guide walks through complete payment flows for Crossmint Agentic Finance, from initial request to on-chain settlement. We’ll cover both simple HTTP paywalls and complex agent-to-agent transactions.
Simple HTTP Paywall Flow
The most basic x402 flow: a client pays to access a protected API endpoint.
Sequence Diagram
Step-by-Step Breakdown
Client Makes Initial Request
Client attempts to access protected endpoint without payment: curl -i http://localhost:3000/ping
Request headers: GET /ping HTTP / 1.1
Host : localhost:3000
Accept : application/json
Server Returns 402 Payment Required
Server detects missing payment, returns requirement: HTTP / 1.1 402 Payment Required
Content-Type : application/json
{
"payment" : {
"amount" : "1000" ,
"currency" : "USDC" ,
"to" : "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" ,
"chainId" : 84532 ,
"network" : "base-sepolia" ,
"facilitator" : "https://x402.org/facilitator"
}
}
See ping/src/server.ts:11-12
Client Signs Payment Message
Client creates EIP-712 signature: const signature = await wallet . signTypedData ({
domain: {
name: "x402 Payment" ,
version: "1" ,
chainId: 84532 ,
verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
},
types: {
Payment: [
{ name: "amount" , type: "uint256" },
{ name: "currency" , type: "address" },
{ name: "to" , type: "address" },
{ name: "nonce" , type: "uint256" }
]
},
primaryType: "Payment" ,
message: {
amount: "1000" ,
currency: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" ,
to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" ,
nonce: Date . now ()
}
});
console . log ( signature );
// 0x1234567890abcdef...
For pre-deployed wallets: Signature includes ERC-6492 wrapper
Client Retries with Payment Header
Client includes signature in retry request: curl -i \
-H "X-PAYMENT: 0x1234567890abcdef..." \
http://localhost:3000/ping
Request headers: GET /ping HTTP / 1.1
Host : localhost:3000
Accept : application/json
X-PAYMENT : 0x1234567890abcdef...
Server Verifies Signature
Server calls facilitator to verify signature: const response = await fetch (
"https://x402.org/facilitator/verify" ,
{
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
signature: "0x1234..." ,
message: {
amount: "1000" ,
currency: "0x036CbD..." ,
to: "0x742d35..." ,
nonce: 1234567890
},
signer: "0xClientWallet"
})
}
);
const { verified , txHash } = await response . json ();
Facilitator Settles On-Chain
Facilitator validates and submits USDC transfer: // 1. Verify signature matches message
const recoveredSigner = recoverTypedDataSigner ({
signature ,
message ,
types ,
domain
});
if ( recoveredSigner !== expectedSigner ) {
throw new Error ( "Invalid signature" );
}
// 2. Check USDC balance
const balance = await usdcContract . balanceOf ( signer );
if ( balance < amount ) {
throw new Error ( "Insufficient USDC balance" );
}
// 3. Submit transfer transaction
const tx = await usdcContract . transfer (
to ,
amount ,
{ from: signer }
);
return { verified: true , txHash: tx . hash };
Server Returns Success
Server responds with data and transaction proof: HTTP / 1.1 200 OK
Content-Type : application/json
X-Transaction-Hash : 0xabc...def
{
"message" : "pong" ,
"transactionHash" : "0xabc...def"
}
Client can verify transaction on Base Sepolia explorer: https://sepolia.basescan.org/tx/0xabc...def
Agent-to-Agent MCP Flow
More complex flow involving two agents communicating via Model Context Protocol (MCP).
Sequence Diagram
Step-by-Step Breakdown
User Requests Action
User asks their agent to perform a paid action: User: "RSVP to event abc-123"
Guest agent detects this requires calling the rsvpToEvent MCP tool.
Guest Agent Calls MCP Tool
Guest makes MCP tool call to Host: const result = await mcpClient . callTool ( "rsvpToEvent" , {
eventId: "abc-123" ,
walletAddress: guestWallet . address
});
MCP Request: {
"jsonrpc" : "2.0" ,
"method" : "tools/call" ,
"params" : {
"name" : "rsvpToEvent" ,
"arguments" : {
"eventId" : "abc-123" ,
"walletAddress" : "0xGuestWallet"
}
}
}
See events-concierge/src/agents/guest.ts
Host Returns 402 Payment Required
Host MCP server checks for payment, finds none: // In Host's paidTool handler
this . server . paidTool (
"rsvpToEvent" ,
"RSVP to an event" ,
0.05 , // Price
{ eventId: z . string (), walletAddress: z . string () },
async ({ eventId , walletAddress }) => {
// This only runs AFTER payment verified
await recordRsvp ( eventId , walletAddress );
}
);
402 Response: {
"statusCode" : 402 ,
"payment" : {
"amount" : "50000" ,
"currency" : "USDC" ,
"to" : "0xHostWallet" ,
"chainId" : 84532 ,
"facilitator" : "https://x402.org/facilitator"
}
}
See events-concierge/src/agents/host.ts:158-202
Guest Confirms Payment with User
Guest agent’s onPaymentRequired callback triggers: mcpClient . withX402Client ({
wallet: guestWallet ,
onPaymentRequired : async ( requirement , retryFn ) => {
// Show modal to user
const approved = await showPaymentModal ({
amount: "0.05 USDC" ,
recipient: "Event Host" ,
action: "RSVP to event abc-123"
});
if ( ! approved ) {
throw new Error ( "Payment declined by user" );
}
// Continue to signing...
}
});
User sees clear payment details and approves.
Guest Wallet Signs Payment
Guest wallet creates ERC-6492 signature (pre-deployed): const signature = await guestWallet . signTypedData ({
domain: {
name: "x402 Payment" ,
version: "1" ,
chainId: 84532 ,
verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
},
types: {
Payment: [
{ name: "amount" , type: "uint256" },
{ name: "currency" , type: "address" },
{ name: "to" , type: "address" },
{ name: "nonce" , type: "uint256" }
]
},
primaryType: "Payment" ,
message: {
amount: "50000" ,
currency: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" ,
to: "0xHostWallet" ,
nonce: Date . now ()
}
});
Signature includes:
Factory address
Deployment bytecode
Inner signature
ERC-6492 magic suffix
Guest Retries with Payment
Guest retries MCP call with X-PAYMENT header: const result = await retryFn ( signature );
MCP Request: {
"jsonrpc" : "2.0" ,
"method" : "tools/call" ,
"params" : {
"name" : "rsvpToEvent" ,
"arguments" : {
"eventId" : "abc-123" ,
"walletAddress" : "0xGuestWallet"
}
},
"headers" : {
"X-PAYMENT" : "0x1234567890abcdef..."
}
}
Host Verifies & Settles Payment
Host calls facilitator to verify signature and settle: const response = await fetch (
` ${ FACILITATOR_URL } /verify` ,
{
method: "POST" ,
body: JSON . stringify ({
signature ,
message: paymentMessage ,
signer: guestWallet . address
})
}
);
const { verified , txHash } = await response . json ();
if ( ! verified ) {
throw new Error ( "Payment verification failed" );
}
Facilitator:
Verifies ERC-6492 signature
Deploys guest wallet (if not already deployed)
Executes USDC transfer from guest to host
Returns transaction hash
Host Records RSVP & Revenue
After payment verified, Host executes business logic: // Record RSVP in KV storage
await kv . put (
` ${ userScopeId } :events: ${ eventId } :rsvps: ${ walletAddress } ` ,
JSON . stringify ({
walletAddress ,
timestamp: Date . now (),
txHash ,
amount: "50000"
})
);
// Increment revenue counter
const revenueKey = ` ${ userScopeId } :revenue` ;
const currentRevenue = parseInt (
await kv . get ( revenueKey ) || "0"
);
await kv . put (
revenueKey ,
( currentRevenue + 50000 ). toString ()
);
See events-concierge/src/shared/eventService.ts
Host Returns Success
Host returns MCP tool result: {
"content" : [{
"type" : "text" ,
"text" : {
"success" : true ,
"eventId" : "abc-123" ,
"eventTitle" : "Tech Meetup" ,
"transactionHash" : "0xabc...def" ,
"message" : "RSVP successful! Paid 0.05 USDC."
}
}]
}
Guest Confirms to User
Guest agent displays confirmation: Guest Agent: ✅ You're registered for "Tech Meetup"!
Guest Agent: 💰 Payment: 0.05 USDC
Guest Agent: 🔗 Transaction: 0xabc...def
Guest Agent: 🌐 Explorer: https://sepolia.basescan.org/tx/0xabc...def
User has blockchain proof of payment and registration.
Wallet Deployment Flow
Special case: Guest wallet’s first payment also deploys the wallet on-chain.
Sequence Diagram
ERC-6492 Signature Structure
// ERC-6492 signature format
[
factoryAddress , // 20 bytes: Wallet factory contract
factoryCalldata , // Variable: Deployment parameters
innerSignature , // 65 bytes: Actual EIP-712 signature
magicSuffix // 32 bytes: "6492649264926492..."
]
Verification process:
function verifyERC6492 ( signature : string , message : any , signer : string ) {
// 1. Check for magic suffix
if ( ! signature . endsWith ( "6492649264926492..." )) {
return verifyStandardSignature ( signature , message , signer );
}
// 2. Extract components
const { factory , calldata , innerSig } = decode ( signature );
// 3. Compute expected wallet address
const expectedAddress = computeCreate2Address ( factory , calldata );
// 4. Verify address matches
if ( expectedAddress !== signer ) {
throw new Error ( "Address mismatch" );
}
// 5. Deploy wallet (if needed)
if ( ! isDeployed ( expectedAddress )) {
await deployContract ( factory , calldata );
}
// 6. Verify inner signature
return verifySignature ( innerSig , message , expectedAddress );
}
See x402Adapter.ts:79-111
Error Scenarios
Real-world payment flows must handle errors gracefully.
Insufficient Balance
Client handling:
try {
const result = await client . get ( "/ping" , { payment: signature });
} catch ( error ) {
if ( error . message . includes ( "Insufficient" )) {
console . log ( "❌ Please add USDC to your wallet" );
console . log ( `🔗 Address: ${ wallet . address } ` );
console . log ( `🪙 Get testnet USDC: https://faucet.circle.com/` );
}
}
Invalid Signature
Common causes:
Wrong chain ID in domain
Incorrect message structure
Tampered signature
Wrong signer address
Network Failure
Retry logic:
async function settlePayment ( signature : string , maxRetries = 3 ) {
for ( let i = 0 ; i < maxRetries ; i ++ ) {
try {
const tx = await chain . submitTransaction ( signature );
return tx . hash ;
} catch ( error ) {
if ( i === maxRetries - 1 ) throw error ;
await sleep ( 1000 * Math . pow ( 2 , i )); // Exponential backoff
}
}
}
Best Practices
Always Verify Server-Side
Never trust client-provided payment claims: // ❌ DON'T
if ( request . headers . get ( "X-PAYMENT-VERIFIED" ) === "true" ) {
executeBusinessLogic ();
}
// ✅ DO
const signature = request . headers . get ( "X-PAYMENT" );
const verified = await facilitator . verify ( signature , message );
if ( verified ) {
executeBusinessLogic ();
}
Keep permanent records of all payments: await kv . put ( `payments: ${ txHash } ` , JSON . stringify ({
from: guestWallet ,
to: hostWallet ,
amount: "50000" ,
timestamp: Date . now (),
eventId: "abc-123" ,
status: "confirmed"
}));
Show Transaction Proofs to Users
Always display blockchain proof: console . log ( `✅ Payment confirmed!` );
console . log ( `Amount: 0.05 USDC` );
console . log ( `TX: ${ txHash } ` );
console . log ( `Explorer: https://sepolia.basescan.org/tx/ ${ txHash } ` );
Handle Pre-Deployed Wallets
Support both pre-deployed and deployed wallets: const signature = await wallet . signTypedData ( message );
// Check if ERC-6492 (pre-deployed)
if ( signature . endsWith ( "6492649264926492..." )) {
console . log ( "⚠️ Wallet will be deployed with first payment" );
}
Use nonces to prevent duplicate payments: const nonce = ` ${ Date . now () } - ${ randomUUID () } ` ;
// Before processing
const alreadyProcessed = await kv . get ( `nonce: ${ nonce } ` );
if ( alreadyProcessed ) {
return { error: "Payment already processed" };
}
// After processing
await kv . put ( `nonce: ${ nonce } ` , "processed" );
Monitoring & Debugging
Log Payment Events
console . log ( "💳 Payment flow started" );
console . log ( ` From: ${ guestWallet } ` );
console . log ( ` To: ${ hostWallet } ` );
console . log ( ` Amount: ${ amount } ` );
try {
const signature = await wallet . signTypedData ( message );
console . log ( `✅ Signature created: ${ signature . substring ( 0 , 20 ) } ...` );
const { verified , txHash } = await facilitator . verify ( signature );
console . log ( `✅ Payment verified` );
console . log ( ` TX Hash: ${ txHash } ` );
console . log ( ` Block: ${ blockNumber } ` );
await recordPayment ({ txHash , amount , ... });
console . log ( `✅ Payment recorded` );
} catch ( error ) {
console . log ( `❌ Payment failed: ${ error . message } ` );
console . log ( ` Stack: ${ error . stack } ` );
}
Track Revenue
// Real-time revenue tracking
const revenue = await kv . get ( ` ${ userId } :revenue` );
const usd = parseInt ( revenue ) / 1000000 ; // Convert from USDC decimals
console . log ( `💰 Total revenue: $ ${ usd . toFixed ( 2 ) } ` );
Monitor Failed Payments
// Log all failures
await kv . put ( `failed-payments: ${ Date . now () } ` , JSON . stringify ({
from: guestWallet ,
to: hostWallet ,
amount ,
error: error . message ,
signature
}));
// Alert on threshold
const failures = await kv . list ({ prefix: "failed-payments:" });
if ( failures . length > 10 ) {
await alertTeam ( "High payment failure rate" );
}
Next Steps
x402 Protocol Learn the HTTP payment protocol
A2A Payments Build agent-to-agent systems
Smart Wallets Deep dive into wallet features
Build Your Own Create your first payment flow
Resources