Skip to main content

Overview

Agent-to-Agent (A2A) payments enable AI agents to autonomously pay each other for services and tool access. No user intervention required after initial approval—agents handle signature generation, payment submission, and settlement verification. Key insight: Agents treat payments like API authentication—just another HTTP header to include.

Why A2A Matters

Traditional payment flows break agent autonomy:

Manual Flows

User must approve every transaction❌ Breaks automation

Private Keys

Agents need direct key access❌ Security nightmare

Custom Infrastructure

Build payment systems from scratch❌ High development cost

User Intervention

Manual signing for each payment❌ Poor UX
A2A payments solve this:
User: "RSVP to event abc-123"

Guest Agent: Detects paid tool, checks price ($0.05)

Guest Agent: Signs USDC payment autonomously

Host Agent: Verifies signature, settles on Base

Guest Agent: "You're registered! TX: 0xabc...def"
Zero user intervention after initial confirmation.

A2A Architecture

A2A payments combine three technologies:

1. x402 Protocol (Payment Layer)

HTTP-native payment protocol using 402 Payment Required status codes.
// Server returns payment requirement
HTTP/1.1 402 Payment Required
{
  "payment": {
    "amount": "50000",  // 0.05 USDC
    "currency": "USDC",
    "to": "0xHostWallet",
    "chainId": 84532
  }
}
See x402 Protocol for details.

2. MCP (Tool Layer)

Model Context Protocol defines how agents call tools and functions.
// Define a paid tool
server.paidTool(
  "rsvpToEvent",
  "RSVP to a paid event",
  0.05,  // Price in USD
  { eventId: z.string() },
  async ({ eventId }) => {
    // Business logic
    return { success: true, eventId };
  }
);
See events-concierge/src/agents/host.ts:158-202

3. Smart Wallets (Identity Layer)

Crossmint smart wallets enable signing without private key management.
// Create wallet from API key
const wallet = await crossmintWallets.createWallet({
  chain: "base-sepolia",
  signer: { type: "api-key" }
});

// Sign payment
const signature = await wallet.signTypedData(paymentMessage);
See Smart Wallets for details.

Payment Flow

Let’s walk through a complete A2A payment:
1

User Initiates Action

User asks their agent to perform a paid action
User: "RSVP to event abc-123"
2

Guest Agent Calls Paid Tool

Agent makes MCP tool call to Host
await mcpClient.callTool("rsvpToEvent", {
  eventId: "abc-123",
  walletAddress: guestWallet.address
});
3

Host Returns 402 Payment Required

Host MCP server detects missing payment, returns requirement
{
  "statusCode": 402,
  "payment": {
    "amount": "50000",
    "currency": "USDC",
    "to": "0xHostWallet",
    "chainId": 84532
  }
}
4

Guest Agent Confirms with User

Agent shows payment details, gets approval
const approved = await askUser(
  `Pay ${requirement.amount} USDC to RSVP?`
);
if (!approved) throw new Error("Payment declined");
5

Guest Agent Signs Payment

Agent creates EIP-712 signature
const signature = await guestWallet.signTypedData({
  domain: { chainId: 84532, ... },
  types: { Payment: [...] },
  message: {
    amount: "50000",
    to: "0xHostWallet",
    currency: "0xUSDC",
    nonce: Date.now()
  }
});
6

Guest Agent Retries with Payment

Agent retries MCP call with X-PAYMENT header
await mcpClient.callTool("rsvpToEvent", args, {
  headers: { "X-PAYMENT": signature }
});
7

Host Verifies & Settles

Host verifies signature and settles payment on-chain
// Verify signature
const valid = await x402.verifySignature(signature, message);

// Settle on Base Sepolia
const tx = await facilitator.settle(signature);

// Execute business logic
await recordRsvp(eventId, guestWallet);
8

Host Returns Success + TX Hash

Host returns tool result with transaction proof
{
  "success": true,
  "eventId": "abc-123",
  "transactionHash": "0xabc...def",
  "message": "RSVP confirmed!"
}
9

Guest Agent Confirms to User

Agent displays confirmation with blockchain proof
Agent: "You're registered for 'Tech Meetup'!"
Agent: "Transaction: 0xabc...def"
Agent: "View on explorer: https://sepolia.basescan.org/tx/0xabc...def"

Code Example: Guest Agent

Here’s how the Guest Agent handles automatic payments:
events-concierge/src/agents/guest.ts
export class Guest extends McpAgent {
  private wallet: Wallet;
  private mcpClient: McpClient;

  async init() {
    // Create guest wallet
    this.wallet = await crossmintWallets.createWallet({
      chain: "base-sepolia",
      signer: { type: "api-key" }
    });

    // Setup MCP client with x402 auto-payment
    this.mcpClient = new McpClient({ url: hostMcpUrl })
      .withX402Client({
        wallet: this.wallet,
        onPaymentRequired: async (requirement, retryFn) => {
          // 1. Show payment modal to user
          const approved = await this.confirmPayment(requirement);
          if (!approved) throw new Error("Payment declined");

          // 2. Sign payment
          const signature = await this.wallet.signPayment(requirement);

          // 3. Retry with signature
          return retryFn(signature);
        }
      });
  }

  async rsvpToEvent(eventId: string) {
    // This automatically handles 402 responses!
    return this.mcpClient.callTool("rsvpToEvent", {
      eventId,
      walletAddress: this.wallet.address
    });
  }
}
See events-concierge/src/agents/guest.ts

Code Example: Host Agent

Here’s how the Host Agent defines paid tools:
events-concierge/src/agents/host.ts
export class Host extends McpAgent {
  private wallet: Wallet;
  private server: McpServer;

  async init() {
    // Create host wallet for receiving payments
    this.wallet = await crossmintWallets.createWallet({
      chain: "base-sepolia",
      signer: { type: "api-key" }
    });

    // Setup MCP server with x402 support
    this.server = new McpServer({ name: "Event RSVP" })
      .withX402({
        network: "base-sepolia",
        recipient: this.wallet.address,
        facilitator: { url: FACILITATOR_URL }
      });

    // Register paid tool
    this.server.paidTool(
      "rsvpToEvent",
      "RSVP to an event (requires payment)",
      0.05,  // Price in USD
      { eventId: z.string(), walletAddress: z.string() },
      async ({ eventId, walletAddress }) => {
        // This only executes AFTER payment is verified!
        const event = await getEvent(eventId);
        await recordRsvp(eventId, walletAddress);

        return {
          success: true,
          eventId,
          eventTitle: event.title,
          message: `RSVP successful! Paid ${event.price} USDC.`
        };
      }
    );
  }
}
See events-concierge/src/agents/host.ts:158-202

Multi-Tenant Architecture

For production A2A systems, use Cloudflare Durable Objects for per-user isolation:
// Each user gets their own Host instance
User A/mcp/users/hash-aHost DO (name: "hash-a")
                               ├─ wallet: 0xAAA...
                               ├─ events: [...]
                               └─ revenue: $12.50

User B/mcp/users/hash-bHost DO (name: "hash-b")
                               ├─ wallet: 0xBBB...
                               ├─ events: [...]
                               └─ revenue: $8.00
Benefits:

Stateful

In-memory state persists across requests

Isolated

Each user has their own wallet and data

Single-threaded

No race conditions or concurrency bugs

Auto-scaling

Created on-demand, hibernate when idle
See events-concierge/README.md for full implementation.

Security Considerations

Always verify signatures match the payment message:
const valid = await verifySignature({
  signature,
  message: paymentRequirement,
  signer: guestWalletAddress
});

if (!valid) throw new Error("Invalid signature");
Use nonces to prevent replay attacks:
const message = {
  amount: "50000",
  to: hostWallet,
  currency: USDC_ADDRESS,
  nonce: Date.now()  // or UUID
};

// Store used nonces
await kv.put(`nonce:${nonce}`, "used");
Verify payer has sufficient funds before executing business logic:
const balance = await getUSDCBalance(guestWallet);
if (balance < requirement.amount) {
  throw new Error("Insufficient USDC balance");
}
Confirm on-chain settlement before marking payment complete:
const tx = await publicClient.getTransaction({
  hash: txHash
});

if (tx.status !== "success") {
  throw new Error("Transaction failed");
}
Prevent abuse with rate limits:
const attempts = await kv.get(`ratelimit:${walletAddress}`);
if (attempts > 10) {
  throw new Error("Rate limit exceeded");
}

Error Handling

Agents must gracefully handle payment failures:
try {
  const result = await mcpClient.callTool("rsvpToEvent", args);
  console.log("✅ RSVP successful:", result);
} catch (error) {
  if (error.code === 402) {
    // Payment required but user declined
    console.log("⚠️ Payment required but not approved");
  } else if (error.message.includes("Insufficient")) {
    // Insufficient USDC balance
    console.log("❌ Please add USDC to your wallet");
  } else if (error.message.includes("signature")) {
    // Invalid signature
    console.log("❌ Signature verification failed");
  } else {
    // Other error
    console.log("❌ RSVP failed:", error.message);
  }
}

Real-World Examples

Event RSVP

MCP-based event booking with autonomous paymentsStack: Cloudflare Durable Objects, MCP, x402

Tweet Agent

Pay to post tweets via AI agentStack: Next.js, Express, x402

Worldstore Agent

Amazon purchases via XMTP chat with gasless USDCStack: XMTP, Base, Crossmint

Ad Bidding

Claude agents competing for ad space with paymentsStack: Claude, x402, autonomous bidding

Best Practices

Use Base Sepolia and Circle’s USDC faucet for development:
Always get user approval before signing payments:
const approved = await showPaymentModal({
  amount: "0.05 USDC",
  recipient: "Event Host",
  action: "RSVP to Tech Meetup"
});
Show users blockchain proof of payment:
console.log(`✅ Payment confirmed!`);
console.log(`TX: ${txHash}`);
console.log(`Explorer: https://sepolia.basescan.org/tx/${txHash}`);
Track all payment attempts for debugging:
await logPayment({
  timestamp: Date.now(),
  from: guestWallet,
  to: hostWallet,
  amount: "50000",
  txHash,
  status: "success"
});
Track earnings in real-time:
const revenue = await kv.get(`${userId}:revenue`);
console.log(`Total revenue: $${revenue / 1000000}`);

Next Steps

x402 Protocol

Deep dive into HTTP payment protocol

Smart Wallets

Learn about Crossmint smart wallet features

Payment Flow

See detailed payment flow diagrams

Build Event RSVP

Build your first A2A payment system

Resources

Build docs developers (and LLMs) love