AI Agent with a $50/day USDC Budget
In this tutorial, you will build an autonomous AI agent that can spend up to $50/day in USDC on Base Sepolia to perform operations like paying suppliers, executing on-chain swaps, or buying resources.
The user authorizes this budget once using their FaceID/TouchID (creating an on-chain Session Key). The agent runs autonomously on the server. If the agent is compromised or behaves unexpectedly, the user can revoke the session in a single transaction, immediately freezing all outbound movement.
We will use the Veridex Agents Framework (@veridex/agents) and Agentic Payments SDK (@veridex/agentic-payments) to build a secure runtime that enforces limits, registers trace-level reasoning for every transaction, and manages context volume to prevent decay.
Architecture Flow
The execution model cleanly separates the user's high-security master key (FaceID/TouchID passkey) from the autonomous server-side agent's key.
Prerequisites
Ensure you have the following installed in your workspace:
- Node.js 18+ or Bun
- A browser supporting WebAuthn/Passkeys (Chrome 108+, Safari 16+)
- A Google Gemini API Key (opens in a new tab) or OpenAI API Key (opens in a new tab)
# Browser packages
npm install @veridex/sdk @veridex/react ethers
# Server packages
npm install @veridex/agents @veridex/agentic-payments zodStep 1: Create the Browser-Side Wallet
The user first needs a Veridex vault. We'll build a React page that registers the user with a passkey, gets their deterministic vault address, and establishes an active developer context.
// frontend/components/AgentProvisioner.tsx
import { usePasskey, useVault } from '@veridex/react';
import { useState } from 'react';
export function AgentProvisioner() {
const { register, credential } = usePasskey();
const { address } = useVault();
const [session, setSession] = useState<any>(null);
if (!credential) {
return (
<div className="vdx-card">
<h3>1. Create Passkey Wallet</h3>
<p>Register a WebAuthn vault on Base Sepolia. No seed phrase required.</p>
<button className="vdx-btn" onClick={() => register('user@example.com', 'My Agent Vault')}>
Create Vault
</button>
</div>
);
}
return (
<div className="vdx-card">
<h3>Vault Active</h3>
<code className="block bg-black/10 p-2 rounded text-sm">{address}</code>
{/* Proceed to Step 2 */}
</div>
);
}Step 2: Mint the $50/Day Session Key
Next, we generate an ephemeral ECDSA session key in the browser, register its authorization parameters on-chain (limit: $50 USD, token: USDC, duration: 24h), and transfer the session credentials to our server.
// frontend/components/AgentProvisioner.tsx (continued)
import { SessionManager, EVMHubClientAdapter, createSDK } from '@veridex/sdk';
import { parseUnits, JsonRpcProvider } from 'ethers';
export function SessionCreator({ sdk, credential }: { sdk: any, credential: any }) {
const [status, setStatus] = useState('');
const createAndRegisterSession = async () => {
setStatus('Creating session key...');
const provider = new JsonRpcProvider('https://sepolia.base.org');
// We use a public relayer adapter to register our session key on-chain gaslessly
const hubClient = new EVMHubClientAdapter(sdk.getChainClient() as any);
const manager = new SessionManager(
credential,
hubClient,
(challenge) => sdk.passkey.sign(challenge),
{
duration: 86400, // 24 hours
// USDC uses 6 decimals on-chain. $50 USDC = 50 * 10^6
maxValue: parseUnits('50', 6),
allowedTokens: ['0x036CbD53842c5426634e7929541eC2318f3dCF7e'] // USDC on Base Sepolia
}
);
const session = await manager.createSession();
setStatus('Syncing with agent server...');
// Post session credentials to the server
const res = await fetch('/api/agent/provision', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vaultAddress: sdk.getVaultAddress(),
sessionCredentials: session.serialize() // Encrypted/serialized session parameters
}),
});
if (res.ok) setStatus('Agent Active and Budgeted!');
else setStatus('Failed to provision agent server.');
};
return (
<div className="vdx-card mt-4">
<h3>2. Authorize $50/Day Budget</h3>
<p>Mints an on-chain Session Key that restricts the agent server's actions.</p>
<button className="vdx-btn vdx-btn-primary" onClick={createAndRegisterSession}>
Authorize Agent
</button>
{status && <p className="mt-2 text-sm">{status}</p>}
</div>
);
}Step 3: Initialize the Server-Side Agent Runtime
On our server, we receive the session credentials and load them into @veridex/agentic-payments to initialize the AgentWallet. We then wrap it in the @veridex/agents general-purpose runtime.
// server/agent.ts
import { createAgent, OpenAIProvider } from '@veridex/agents';
import { createAgentWallet } from '@veridex/agentic-payments';
import { createSDK } from '@veridex/sdk';
import { payTool, balanceTool } from './tools';
export async function startAutonomousAgent(vaultAddress: string, sessionCredentialsHex: string) {
// 1. Initialize core Veridex protocol primitives
const sdk = createSDK('base', { network: 'testnet' });
const wallet = await createAgentWallet({
masterCredential: {
credentialId: 'user-credential-id',
publicKeyX: 0n,
publicKeyY: 0n,
keyHash: '0xhash...'
},
session: {
dailyLimitUSD: 50,
perTransactionLimitUSD: 10,
expiryHours: 24,
allowedChains: [10004] // Base Sepolia Wormhole chain ID
}
});
// Import the session payload received from the browser
await wallet.importSession(JSON.parse(sessionCredentialsHex));
// 2. Initialize the safe agentic runtime
const agent = createAgent(
{
id: `agent-${vaultAddress.slice(0, 8)}`,
name: 'Budgeted Swapper & Purchaser',
model: { provider: 'openai', model: 'gpt-4o' },
instructions: `
You are an autonomous treasury and purchasing assistant.
You have an active daily limit of $50 USDC.
You spend autonomously to execute the user's tasks.
Before every transfer, explain your exact reasoning to the user.
Never exceed your daily cap.
`,
tools: [
payTool(wallet),
balanceTool(wallet)
],
// Hard stop after 8 reasoning loops to prevent cost or loop runaways
maxTurns: 8,
policies: [
// Policies are evaluated in the runtime before tool invocation
{ type: 'maxRunSpendUSD', params: { limit: 50 } },
{ type: 'blockSafetyClasses', params: { safetyClasses: ['destructive', 'privileged'] } }
]
},
{
modelProviders: {
openai: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
},
enableTracing: true,
enableCheckpoints: true
}
);
return agent;
}Step 4: Define Budget-Aware Tools
Every tool provided to @veridex/agents must specify a strict input schema using Zod and a designated safety class. This ensures that the policy engine can filter and restrict calls before execution starts.
// server/tools.ts
import { tool } from '@veridex/agents';
import { AgentWallet } from '@veridex/agentic-payments';
import { parseUnits } from 'ethers';
import { z } from 'zod';
export const balanceTool = (wallet: AgentWallet) =>
tool({
name: 'check_usdc_balance',
description: 'Check your available USDC balance on Base',
input: z.object({}),
safetyClass: 'read',
async execute() {
// getBalance expects a Wormhole Chain ID (e.g., 10004 for Base Sepolia) and returns TokenBalance[]
const balances = await wallet.getBalance(10004);
const usdc = balances.find((b: any) => b.token.symbol === 'USDC');
const formattedBalance = usdc ? usdc.formatted : '0';
const status = wallet.getSessionStatus();
return {
success: true,
llmOutput: `Available balance: ${formattedBalance} USDC. Limit spent today: $${status.totalSpentUSD}/$50.`,
};
},
});
export const payTool = (wallet: AgentWallet) =>
tool({
name: 'make_usdc_payment',
description: 'Autonomous payment in USDC up to your daily budget limit',
input: z.object({
recipient: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM address'),
amountUSD: z.number().positive().max(50, 'Cannot exceed daily limit'),
purpose: z.string().min(5, 'A clear reason is required for trace auditing'),
}),
safetyClass: 'financial',
async execute({ input }) {
try {
const status = wallet.getSessionStatus();
// Enforce reasoning capture
const traceReasoning = `Autonomous Payment: ${input.purpose}. Daily cumulative spent: $${status.totalSpentUSD}`;
// Convert to smallest units as string
const atomicAmount = parseUnits(input.amountUSD.toString(), 6).toString();
const tx = await wallet.pay({
recipient: input.recipient,
amount: atomicAmount, // USDC uses 6 decimals, pay expects smallest unit string
token: 'USDC',
chain: 10004, // Base Sepolia Wormhole chain ID
metadata: { reasoning: traceReasoning }
});
return {
success: true,
llmOutput: `Successfully transferred ${input.amountUSD} USDC to ${input.recipient}. Hash: ${tx.txHash}`,
};
} catch (err: any) {
return {
success: false,
llmOutput: `Payment failed: ${err.message}`,
};
}
},
});Step 5: Run the Agent & Prevent Context Explosion
We run the agent simply by calling agent.run(prompt).
The Root Theorem of Context Engineering (Schick, 2026) dictates that an agent's reasoning performance and tool precision degrade monotonically as its in-context token count increases. To avoid hitting this "degradation cliff" over long-running sessions, @veridex/agents includes an active Context Compiler.
When executing a run, Veridex continuously monitors the token footprint of the run transcript. If the conversation thread exceeds the effective context window (), the ContextCompiler automatically:
- Summarizes older turn segments.
- Compresses tool outputs into high-level declarative state.
- Sheds redundant intermediary tokens.
- Distills episodic observations into permanent Semantic Memory records.
// server/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { startAutonomousAgent } from './agent';
export async function POST(req: NextRequest) {
const { vaultAddress, sessionCredentials, prompt } = await req.json();
const agent = await startAutonomousAgent(vaultAddress, sessionCredentials);
// Compile and run the agent loop
const result = await agent.run(prompt);
// Return the terminal outcome and the audit-ready Trace Log
return NextResponse.json({
output: result.output,
state: result.run.state, // e.g. "completed" | "suspended"
traceId: result.run.traceId,
tokensUsed: result.run.tokensUsed
});
}What Happens During a Compromise?
If the agent server is hacked, or if a Tool Poisoning Attack (TPA) tricks the model into making unintended payments, multiple nested defense lines trigger immediately:
| Layer | Defense Mechanism | Failure Outcome |
|---|---|---|
| L1: Model Instructions | Reasoning constraints within the prompt. | Weakest; can be bypassed via advanced jailbreaks. |
| L2: Runtime Policy Engine | Restricts tool safety classes (maxRunSpendUSD). | Blocked. The runtime denies the tool proposal before the model can execute it. |
| L3: Smart Contract Session | On-chain verification of daily spending cap ($50 USDC). | Blocked. The Base blockchain refuses to execute the transaction, regardless of model instructions. |
| L4: Instant Revocation | One-click browser tx revoking the ephemeral session key. | Dead Switch. All future agent server requests fail to sign on-chain. |
To learn how to monitor active agent reasoning traces or export signed evidence packages to a SIEM platform, proceed to the Operator Quickstart.