Learn how to build an AI agent that can autonomously purchase Zora Creator Coins using secure Spend Permissions on Base. This example demonstrates how to combine Base Accountโ€™s Spend Permissions with Coinbase Developer Platform (CDP) Server Wallets and Trade API for seamless, gas-free AI agent transactions.
Spend Permissions Agent Interface

Overview

This example showcases a complete AI agent implementation that:
Production Security: This example uses simplified session management for demonstration. In production, implement proper JWT tokens with secure session secrets.

Key Features

๐Ÿ” Secure Authentication

The authentication flow combines frontend wallet connection with backend signature verification:
// Frontend: Sign-In with Ethereum implementation
const provider = createBaseAccountSDK({
  appName: "Zora Creator Coins Agent",
}).getProvider();

// 1. Get nonce from server
const nonceResponse = await fetch('/api/auth/verify', { method: 'GET' });
const { nonce } = await nonceResponse.json();

// 2. Connect with SIWE capability
const connectResponse = await provider.request({
  method: "wallet_connect",
  params: [{
    version: "1",
    capabilities: {
      signInWithEthereum: {
        chainId: '0x2105',
        nonce,
      },
    },
  }],
});

// 3. Verify signature on server
const verifyResponse = await fetch('/api/auth/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ address, message, signature })
});

๐Ÿ’ธ Spend Permission Management

Users can grant limited spending authority to the agent:
// Frontend: Request spend permission from user
import { requestSpendPermission } from "@base-org/account/spend-permission";
import { createBaseAccountSDK } from "@base-org/account";

const handleSetupPermission = async () => {
  // Get server wallet address
  const walletResponse = await fetch("/api/wallet/create", { method: "POST" });
  const { smartAccountAddress } = await walletResponse.json();

  // Request spend permission
  const permission = await requestSpendPermission({
    account: userAddress as `0x${string}`,
    spender: smartAccountAddress as `0x${string}`,
    token: USDC_BASE_ADDRESS as `0x${string}`,
    chainId: 8453,
    allowance: BigInt(dailyLimit * 1_000_000), // Convert USD to USDC (6 decimals)
    periodInDays: 1,
    provider: createBaseAccountSDK({
      appName: "Zora Creator Coins Agent",
    }).getProvider(),
  });

  // Store permission for later use
  localStorage.setItem("spendPermission", JSON.stringify(permission));
};
Spend permissions are granted for USDC on Base mainnet with daily limits between 1โˆ’1-2, making it safe for testing and demonstrations.

๐Ÿค– AI Agent Integration

The agent processes natural language requests and executes transactions:
// AI function definition for creator coin purchases
export const ZORA_BUY_FUNCTION = {
  type: 'function' as const,
  function: {
    name: 'buy_zora_coin',
    description: 'Buy a Zora creator coin for a specific user handle and amount',
    parameters: {
      type: 'object',
      properties: {
        zoraHandle: {
          type: 'string',
          description: 'The Zora user handle or identifier to buy coins for',
        },
        amountUSD: {
          type: 'number',
          description: 'The amount in USD to spend on the creator coin',
        },
      },
      required: ['zoraHandle', 'amountUSD'],
    },
  },
};

export async function generateChatResponse(
  messages: ChatMessage[],
  tools: any[] = [ZORA_BUY_FUNCTION]
) {
  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [
      { role: 'system', content: SYSTEM_PROMPT },
      ...messages,
    ],
    tools,
    tool_choice: 'auto',
    max_completion_tokens: 1000,
  });

  return response;
}

โ›ฝ Gas-Free Transactions

All transactions are sponsored using CDP Paymaster:
src/app/api/zora/buy/route.ts
// Backend: Gas-sponsored transaction execution
import { prepareSpendCallData } from '@base-org/account/spend-permission';

export async function POST(request: NextRequest) {
  const { zoraHandle, amountUSD, permission } = await request.json();
  
  // Convert USD to USDC (6 decimals)
  const amountUSDC = BigInt(Math.floor(amountUSD * 1_000_000));
  
  // Prepare spend calls using the permission
  const spendCalls = await prepareSpendCallData(permission, amountUSDC);
  
  // Execute with gas sponsorship
  const result = await sendCalls({
    calls: spendCalls,
    capabilities: {
      paymasterService: {
        url: process.env.PAYMASTER_URL,
      },
    },
  });
  
  return NextResponse.json({ 
    success: true, 
    transactionHash: result.hash,
    message: `Successfully purchased ${amountUSD} USDC worth of @${zoraHandle}'s creator coin!`
  });
}

Implementation Details

Authentication Flow

1

Nonce Generation

Server generates a secure nonce for the authentication challenge
2

Signature Request

User signs a SIWE message containing the nonce with their Base Account
3

Signature Verification

Server verifies the signature and nonce to establish a secure session
4

Session Creation

Authenticated session is created with secure cookies

Spend Permission Workflow

AI Agent Transaction Flow

Code Structure

Frontend Components

// Base Account authentication with SIWE
import { createBaseAccountSDK } from "@base-org/account";

export const SignInWithBaseButton = ({ onSignIn, colorScheme = "light" }) => {
  const [isLoading, setIsLoading] = useState(false);

  const handleSignIn = async () => {
    setIsLoading(true);
    try {
      const provider = createBaseAccountSDK({
        appName: "Zora Creator Coins Agent",
      }).getProvider();

      // 1. Get nonce from server
      const nonceResponse = await fetch('/api/auth/verify', { method: 'GET' });
      const { nonce } = await nonceResponse.json();
      
      // 2. Connect with SIWE capability
      const connectResponse = await provider.request({
        method: "wallet_connect",
        params: [{
          version: "1",
          capabilities: {
            signInWithEthereum: {
              chainId: '0x2105',
              nonce,
            },
          },
        }],
      });

      const { address } = connectResponse.accounts[0];

      // 3. Handle SIWE or fallback to manual signing
      if (connectResponse.signInWithEthereum) {
        const { message, signature } = connectResponse.signInWithEthereum;
        
        await fetch('/api/auth/verify', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ address, message, signature })
        });
      }

      onSignIn(address);
    } catch (err) {
      console.error("Sign in failed:", err);
    } finally {
      setIsLoading(false);
    }
  };
};

Backend API Routes

// Authentication endpoint with signature verification
import { NextRequest, NextResponse } from 'next/server';
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';

const client = createPublicClient({ chain: base, transport: http() });
const nonces = new Set<string>();

export async function POST(request: NextRequest) {
  const { address, message, signature } = await request.json();
  
  // Extract and validate nonce
  const nonce = message.match(/Nonce: (\w+)/)?.[1];
  if (!nonce || !nonces.has(nonce)) {
    return NextResponse.json({ error: 'Invalid or expired nonce' }, { status: 401 });
  }
  
  nonces.delete(nonce); // Prevent reuse
  
  // Verify signature using viem
  const isValid = await client.verifyMessage({ 
    address: address as `0x${string}`, 
    message, 
    signature: signature as `0x${string}` 
  });
  
  if (isValid) {
    const sessionToken = Buffer.from(`${address}:${Date.now()}`).toString('base64');
    const response = NextResponse.json({ ok: true, address, sessionToken });
    
    response.cookies.set('session', sessionToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 60 * 60 * 24 * 7
    });
    
    return response;
  }
}

export async function GET() {
  // Generate secure nonce
  const array = new Uint8Array(16);
  crypto.getRandomValues(array);
  const nonce = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
  
  nonces.add(nonce);
  return NextResponse.json({ nonce });
}

Environment Variables

Set up these environment variables for deployment:
NEXT_PUBLIC_SITE_URL=https://your-app.vercel.app
OPENAI_API_KEY=your_openai_api_key
ZORA_API_KEY=your_zora_api_key
PAYMASTER_URL=https://api.developer.coinbase.com/rpc/v1/base/your_key
SESSION_SECRET=your_secure_random_session_secret

Deployment

Using Vercel CLI

Deploy directly from the project subfolder:
cd base-account/agent-spend-permissions
npx vercel --prod

Environment Setup

  1. Add environment variables in Vercel dashboard
  2. Set root directory to base-account/agent-spend-permissions
  3. Framework should auto-detect as Next.js
For monorepo deployments, using Vercel CLI from the subfolder is more reliable than configuring root directory in the dashboard.

Usage Examples

Basic Creator Coin Purchase

User: "Buy $1.50 worth of @vitalik's creator coin"
Agent: "I'll purchase $1.50 worth of @vitalik's creator coin for you..."
       โœ… Purchase completed! Coins transferred to your wallet.

Permission Management

User: Views active permissions in right panel
      - Daily Limit: $2.00 USDC โ€ข Active
      - [Revoke] button available
      
User: Clicks "Revoke" โ†’ Wallet popup โ†’ Permission revoked
Agent: "โœ… Spend permission revoked successfully!"

Next Steps

1

Clone the Repository

git clone https://github.com/base/demos.git
cd demos/base-account/agent-spend-permissions
2

Install Dependencies

npm install
3

Configure Environment

Set up your environment variables for OpenAI, Zora, and CDP services
4

Run Development Server

npm run dev
5

Test the Flow

Sign in, set up spend permissions, and chat with the AI agent
Want to customize? You can modify the AI prompts, spending limits, supported tokens, or integrate with different creator platforms by updating the respective components and API routes.