Skip to main content

Event RSVP (Events Concierge)

A production-ready reference implementation showing how AI agents can autonomously pay for MCP tool calls using smart wallets, Cloudflare Durable Objects, and the x402 payment protocol.
This is the most comprehensive production example in the repository, demonstrating advanced patterns for multi-tenant agent systems with real on-chain payments.

Overview

The Events Concierge enables users to create paid events and allows AI agents to autonomously RSVP by making USDC payments on Base Sepolia. Unlike simpler demos, this implements:
  • Per-user isolation using Cloudflare Durable Objects
  • Multi-tenant architecture with separate wallets per host
  • Production-grade payment verification with ERC-6492 and EIP-1271
  • Stateful agent connections maintained across requests
  • Real blockchain settlements on Base Sepolia

What Makes This Different

Real Payments

Actual on-chain USDC transactions on Base—no mocks, no simulations

Multi-Tenant

Each user gets their own MCP server instance with isolated wallet and data

Stateful Agents

Durable Objects maintain connection state, eliminating race conditions

Production Patterns

Error handling, retry logic, signature verification, and deployment strategies

Architecture

Core Technologies

1. Cloudflare Durable Objects

Durable Objects provide single-threaded, stateful mini-servers per user:
export class Host extends DurableObject {
  private wallet: Wallet;
  private server: McpServer;
  private userScopeId: string;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    
    // Extract user ID from DO name
    const headers = new Headers(request.headers);
    this.userScopeId = headers.get("x-user-scope-id");
    
    // Each user gets their own wallet
    this.wallet = await createHostWallet(env, this.userScopeId);
    
    // Each user gets their own MCP server
    this.server = new McpServer({ name: `Events-${this.userScopeId}` })
      .withX402({ wallet: this.wallet, ... });
  }
}
Why Durable Objects?
NeedRegular WorkerDurable Object
MCP connection stateLost between requestsPersists in memory
WebSocket supportLimitedBuilt-in
Per-user isolationShared instanceUnique per ID
CoordinationRace conditionsSingle-threaded

2. MCP with x402 Payments

MCP (Model Context Protocol) allows agents to call tools. x402 adds payment requirements:
// Host defines what tools require payment
this.server.paidTool(
  "rsvpToEvent",
  "RSVP to a paid event",
  0.05,  // Price in USD
  { eventId: z.string() },
  {},
  async ({ eventId }, paymentTx: TransactionReceipt) => {
    // Only executed AFTER payment verification!
    
    const event = await eventService.getEvent(eventId);
    await eventService.recordRsvp({
      eventId,
      guestWallet: paymentTx.from,
      txHash: paymentTx.hash,
      amount: "50000" // 0.05 USDC (6 decimals)
    });
    
    return {
      success: true,
      event,
      transactionHash: paymentTx.hash
    };
  }
);

3. Crossmint Smart Wallets

Crossmint wallets work before deployment using ERC-6492 signatures:
// Create wallet (not deployed yet!)
const wallet = await crossmintWallets.createWallet({
  chain: "base-sepolia",
  signer: { type: "api-key" }
});

console.log(wallet.address); // 0x123... (counterfactual address)

// Can sign payments before deployment
const signature = await wallet.signTypedData({
  domain: { chainId: 84532, ... },
  types: { Payment: [...] },
  message: { amount: "50000", to: "0xabc...", ... }
});
// Returns ERC-6492 signature (includes deployment bytecode)

// First payment auto-deploys the wallet
const tx = await facilitator.settle(signature);
// Wallet is now deployed and owns itself!
Signature Standards:
StageStandardHow It Works
Pre-deployedERC-6492Signature includes deployment bytecode. Verifier simulates deployment.
DeployedEIP-1271Contract’s isValidSignature() validates signatures.

4. Per-User Data Isolation

KV storage is scoped by user ID:
// User registration creates URL-safe hash
const urlSafeId = await hashUserId(email); // "a3f2c1b4..."

// Store user mapping
await env.SECRETS.put(`users:${email}`, JSON.stringify({
  userId: email,
  walletAddress: wallet.address,
  urlSafeId
}));

await env.SECRETS.put(`usersByHash:${urlSafeId}`, ...);

// Events scoped by urlSafeId
await env.SECRETS.put(
  `${urlSafeId}:events:${eventId}`,
  JSON.stringify(eventData)
);

// Revenue tracking
await env.SECRETS.put(
  `${urlSafeId}:revenue`,
  totalRevenue.toString()
);
Routing:
User A → /mcp/users/a3f2c1b4 → Host DO (name: "a3f2c1b4")
                                 ├─ wallet: 0xAAA...
                                 ├─ events: [event1, event2]
                                 └─ revenue: $12.50

User B → /mcp/users/b8e9d6f1 → Host DO (name: "b8e9d6f1")
                                 ├─ wallet: 0xBBB...
                                 ├─ events: [event3]
                                 └─ revenue: $3.00

Payment Flow Deep Dive

1

Guest Calls Paid Tool

await x402Client.callTool(onPaymentRequired, {
  name: "rsvpToEvent",
  arguments: { eventId: "event-123" }
});
2

Host Returns 402

{
  "statusCode": 402,
  "payment": {
    "amount": "50000",
    "currency": "USDC",
    "to": "0xHostWallet...",
    "chainId": 84532,
    "facilitator": "https://x402.org/facilitator"
  }
}
3

Guest Confirms Payment

User sees modal with payment details and clicks “Approve”.
4

Guest Signs EIP-712

const signature = await wallet.signTypedData({
  domain: { name: "x402 Payment", version: "1", chainId: 84532 },
  types: {
    Payment: [
      { name: "amount", type: "uint256" },
      { name: "currency", type: "address" },
      { name: "to", type: "address" }
    ]
  },
  message: {
    amount: "50000",
    currency: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    to: "0xHostWallet..."
  }
});
5

Guest Retries with Signature

await x402Client.callTool(onPaymentRequired, {
  name: "rsvpToEvent",
  arguments: { eventId: "event-123" }
}, {
  headers: { "X-PAYMENT": signature }
});
6

Facilitator Verifies + Settles

  • Verify signature matches message
  • Check guest has USDC balance
  • Submit transferFrom transaction
  • Return TX hash
7

Host Records RSVP

await eventService.recordRsvp({
  eventId: "event-123",
  guestWallet: "0xGuestWallet...",
  txHash: "0xabc...def",
  amount: "50000"
});

await incrementRevenue("50000");
8

Guest Receives Confirmation

{
  "success": true,
  "event": { "id": "event-123", "title": "Web3 Meetup" },
  "transactionHash": "0xabc...def",
  "message": "RSVP confirmed! Paid 0.05 USDC."
}

Key Features

Free Tools

// List events (no payment required)
this.server.tool(
  "listEvents",
  "List all available events",
  {},
  {},
  async () => {
    const events = await eventService.listEvents();
    return { events };
  }
);
// RSVP requires $0.05 payment
this.server.paidTool(
  "rsvpToEvent",
  "RSVP to an event",
  0.05,
  { eventId: z.string() },
  {},
  async ({ eventId }, tx) => {
    // Payment already verified!
    await recordRsvp(eventId, tx);
  }
);

Analytics Tool

// Get event stats for $0.01
this.server.paidTool(
  "getEventAnalytics",
  "Get detailed event analytics",
  0.01,
  { eventId: z.string() },
  {},
  async ({ eventId }) => {
    const rsvps = await getRsvps(eventId);
    return {
      totalRsvps: rsvps.length,
      totalRevenue: rsvps.reduce((sum, r) => sum + parseFloat(r.amount), 0),
      rsvps
    };
  }
);

Production Deployment

1

Create KV Namespace

npx wrangler kv:namespace create "SECRETS"
# Copy the ID to wrangler.toml
2

Set Secrets

npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put CROSSMINT_API_KEY
3

Configure Durable Objects

# wrangler.toml
[[durable_objects.bindings]]
name = "Host"
class_name = "Host"
script_name = "events-concierge"

[[durable_objects.bindings]]
name = "Guest"
class_name = "Guest"
script_name = "events-concierge"
4

Deploy

npm run deploy
# App live at https://events-concierge.YOUR-SUBDOMAIN.workers.dev
5

Switch to Mainnet

Update src/constants.ts:
export const CHAIN_ID = 8453; // Base mainnet
export const NETWORK = "base";
export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
Redeploy:
npm run deploy

Use Cases

Paid API Access

Monetize MCP tools without subscription models. Pay-per-use for data, AI inference, or compute.

Event Ticketing

Sell event tickets through AI agents. Automated RSVP management with on-chain receipts.

Premium Features

Unlock advanced agent capabilities with micropayments. Free tier + paid upgrades.

Agent Marketplaces

Build platforms where agents discover and pay for third-party tools autonomously.

Learn More

Source Code

View the complete implementation

Durable Objects

Learn about stateful agent architecture

MCP Protocol

Explore the Model Context Protocol

x402 Spec

Understand HTTP payment protocol

Build docs developers (and LLMs) love