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.
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:
src/components/SignInWithBase.tsx
src/app/api/auth/verify/route.ts
// 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:
src/components/SpendPermissionSetup.tsx
src/lib/spend-permissions.ts
// 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- 1 โ 2, making it safe for testing and demonstrations.
๐ค AI Agent Integration
The agent processes natural language requests and executes transactions:
src/lib/openai.ts
src/components/ChatInterface.tsx
// 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
Nonce Generation
Server generates a secure nonce for the authentication challenge
Signature Request
User signs a SIWE message containing the nonce with their Base Account
Signature Verification
Server verifies the signature and nonce to establish a secure session
Session Creation
Authenticated session is created with secure cookies
Spend Permission Workflow
AI Agent Transaction Flow
Code Structure
Frontend Components
src/components/SignInWithBase.tsx
src/components/SpendPermissionSetup.tsx
src/components/SpendPermissionManager.tsx
// 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
src/app/api/auth/verify/route.ts
src/app/api/wallet/create/route.ts
src/app/api/zora/buy/route.ts
src/app/api/chat/route.ts
// 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
Add environment variables in Vercel dashboard
Set root directory to base-account/agent-spend-permissions
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
Clone the Repository
git clone https://github.com/base/demos.git
cd demos/base-account/agent-spend-permissions
Configure Environment
Set up your environment variables for OpenAI, Zora, and CDP services
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.