Skip to main content

Multi-Agent Budget Coordination

Coordinate multiple AI agents that share a common budget pool with spending limits and approval workflows.

Multi-Agent Budget Architecture
DifficultyIntermediate
Time30 minutes
PrerequisitesQuick Start, Autonomous Agent Guide

Problemโ€‹

You have a team of AI agents that need to:

  • Share a common budget pool
  • Each agent has individual spending limits
  • Large purchases need approval
  • Track spending across all agents
  • Prevent overspending

Think: A research crew where each agent can buy data/compute, but the total budget is shared.


Solutionโ€‹

Create a Budget Coordinator that manages funds and authorizes spending for sub-agents.

TL;DR

Central treasury wallet โ†’ Agents request spending โ†’ Coordinator checks limits โ†’ Auto-approve small, flag large โ†’ Track everything.

Understanding Settlement

Who settles? Either party can trigger settlement:

  • Consumer: Can call releaseEscrow() anytime after delivery
  • Provider: Can call after the dispute window expires (default: 2 days)
  • Automated: Platform bots monitor and settle eligible transactions

Timeline: Typically 2-5 minutes after dispute window closes on testnet. Mainnet may vary based on gas conditions.

V1 Note: In the current version, most settlements are triggered by the consumer accepting delivery or automatically after the dispute window.


Complete Codeโ€‹

Budget Coordinatorโ€‹

src/budget-coordinator.ts
// Level 2: Advanced API - Direct protocol control
import { ACTPClient, State } from '@agirails/sdk';
import { parseUnits, formatUnits } from 'ethers';

interface AgentConfig {
id: string;
name: string;
address: string;
spendingLimit: bigint; // Per-transaction limit
dailyLimit: bigint; // Daily spending cap
requiresApproval: bigint; // Threshold for manual approval
}

interface SpendingRecord {
agentId: string;
amount: bigint;
txId: string;
timestamp: number;
provider: string;
purpose: string;
}

class BudgetCoordinator {
private client: ACTPClient;
private agents: Map<string, AgentConfig> = new Map();
private spending: SpendingRecord[] = [];
private totalBudget: bigint;
private pendingApprovals: Map<string, SpendingRequest> = new Map();

constructor(client: ACTPClient, totalBudget: bigint) {
this.client = client;
this.totalBudget = totalBudget;
}

// Register an agent with spending limits
registerAgent(config: AgentConfig): void {
this.agents.set(config.id, config);
console.log(`โœ… Registered agent: ${config.name}`);
console.log(` Per-tx limit: ${formatUnits(config.spendingLimit, 6)} USDC`);
console.log(` Daily limit: ${formatUnits(config.dailyLimit, 6)} USDC`);
}

// Request spending authorization
async requestSpending(request: SpendingRequest): Promise<SpendingResponse> {
const agent = this.agents.get(request.agentId);

if (!agent) {
return {
approved: false,
reason: 'Agent not registered'
};
}

// Check 1: Per-transaction limit
if (request.amount > agent.spendingLimit) {
return {
approved: false,
reason: `Amount ${formatUnits(request.amount, 6)} exceeds per-tx limit ${formatUnits(agent.spendingLimit, 6)}`
};
}

// Check 2: Daily limit
const dailySpent = this.getDailySpending(request.agentId);
if (dailySpent + request.amount > agent.dailyLimit) {
return {
approved: false,
reason: `Would exceed daily limit. Spent: ${formatUnits(dailySpent, 6)}, Limit: ${formatUnits(agent.dailyLimit, 6)}`
};
}

// Check 3: Total budget
const totalSpent = this.getTotalSpending();
if (totalSpent + request.amount > this.totalBudget) {
return {
approved: false,
reason: `Would exceed total budget. Spent: ${formatUnits(totalSpent, 6)}, Budget: ${formatUnits(this.totalBudget, 6)}`
};
}

// Check 4: Requires approval?
if (request.amount > agent.requiresApproval) {
const approvalId = this.createApprovalRequest(request);
return {
approved: false,
requiresApproval: true,
approvalId: approvalId,
reason: `Amount exceeds auto-approval threshold. Approval ID: ${approvalId}`
};
}

// All checks passed - execute spending
return await this.executeSpending(request, agent);
}

private async executeSpending(
request: SpendingRequest,
agent: AgentConfig
): Promise<SpendingResponse> {
try {
// Create transaction on behalf of requester
const txId = await this.client.advanced.createTransaction({
requester: await this.client.getAddress(), // Coordinator pays
provider: request.provider,
amount: request.amount,
deadline: Math.floor(Date.now() / 1000) + 3600,
disputeWindow: 3600,
metadata: '0x'
});

// Fund escrow (approve + link)
await this.client.advanced.linkEscrow(txId);

// Record spending
this.spending.push({
agentId: request.agentId,
amount: request.amount,
txId: txId,
timestamp: Date.now(),
provider: request.provider,
purpose: request.purpose
});

console.log(`๐Ÿ’ธ Spending approved for ${agent.name}`);
console.log(` Amount: ${formatUnits(request.amount, 6)} USDC`);
console.log(` Provider: ${request.provider}`);
console.log(` Transaction: ${txId}`);
console.log(` Settlement: Admin/bot will execute SETTLED (requester anytime; provider after dispute window)`);

return {
approved: true,
txId: txId,
remainingDaily: agent.dailyLimit - this.getDailySpending(agent.id),
remainingTotal: this.totalBudget - this.getTotalSpending()
};

} catch (error) {
return {
approved: false,
reason: `Execution failed: ${error.message}`
};
}
}

// Get agent's spending for today
private getDailySpending(agentId: string): bigint {
const today = new Date().setHours(0, 0, 0, 0);

return this.spending
.filter(s => s.agentId === agentId && s.timestamp >= today)
.reduce((sum, s) => sum + s.amount, 0n);
}

// Get total spending across all agents
private getTotalSpending(): bigint {
return this.spending.reduce((sum, s) => sum + s.amount, 0n);
}

// Create pending approval request
private createApprovalRequest(request: SpendingRequest): string {
const approvalId = `approval-${Date.now()}`;
this.pendingApprovals.set(approvalId, request);
return approvalId;
}

// Manual approval (called by human or senior agent)
async approveSpending(approvalId: string): Promise<SpendingResponse> {
const request = this.pendingApprovals.get(approvalId);
if (!request) {
return { approved: false, reason: 'Approval not found' };
}

const agent = this.agents.get(request.agentId)!;
this.pendingApprovals.delete(approvalId);

return await this.executeSpending(request, agent);
}

// Reject pending approval
rejectSpending(approvalId: string, reason: string): void {
this.pendingApprovals.delete(approvalId);
console.log(`โŒ Spending rejected: ${reason}`);
}

// Get spending report
getReport(): BudgetReport {
const byAgent = new Map<string, bigint>();

for (const record of this.spending) {
const current = byAgent.get(record.agentId) || 0n;
byAgent.set(record.agentId, current + record.amount);
}

return {
totalBudget: this.totalBudget,
totalSpent: this.getTotalSpending(),
remaining: this.totalBudget - this.getTotalSpending(),
byAgent: Object.fromEntries(
Array.from(byAgent.entries()).map(([id, amount]) => [
id,
formatUnits(amount, 6)
])
),
pendingApprovals: Array.from(this.pendingApprovals.keys())
};
}
}

interface SpendingRequest {
agentId: string;
amount: bigint;
provider: string;
purpose: string;
}

interface SpendingResponse {
approved: boolean;
txId?: string;
reason?: string;
requiresApproval?: boolean;
approvalId?: string;
remainingDaily?: bigint;
remainingTotal?: bigint;
}

interface BudgetReport {
totalBudget: bigint;
totalSpent: bigint;
remaining: bigint;
byAgent: Record<string, string>;
pendingApprovals: string[];
}

Agent Implementationโ€‹

src/budgeted-agent.ts
// Level 2: Advanced API - Direct protocol control
class BudgetedAgent {
private agentId: string;
private coordinator: BudgetCoordinator;
private client: ACTPClient;

constructor(
agentId: string,
coordinator: BudgetCoordinator,
client: ACTPClient
) {
this.agentId = agentId;
this.coordinator = coordinator;
this.client = client;
}

// Request to spend from shared budget
async purchaseService(
provider: string,
amount: bigint,
purpose: string
): Promise<{ success: boolean; txId?: string; error?: string }> {
console.log(`๐Ÿค– Agent ${this.agentId} requesting spend...`);

const response = await this.coordinator.requestSpending({
agentId: this.agentId,
amount: amount,
provider: provider,
purpose: purpose
});

if (response.approved) {
console.log(`โœ… Approved! Transaction: ${response.txId}`);
return { success: true, txId: response.txId };
}

if (response.requiresApproval) {
console.log(`โณ Requires approval: ${response.approvalId}`);
return {
success: false,
error: `Pending approval: ${response.approvalId}`
};
}

console.log(`โŒ Denied: ${response.reason}`);
return { success: false, error: response.reason };
}
}

Main Setupโ€‹

src/main.ts
// Level 2: Advanced API - Direct protocol control
async function main() {
// Initialize coordinator with treasury wallet
const coordinatorClient = await ACTPClient.create({
mode: 'testnet',
requesterAddress: process.env.TREASURY_ADDRESS!,
privateKey: process.env.TREASURY_PRIVATE_KEY!
});

// Create coordinator with $1000 budget
const coordinator = new BudgetCoordinator(
coordinatorClient,
parseUnits('1000', 6)
);

// Register agents with their limits
coordinator.registerAgent({
id: 'research-agent',
name: 'Research Agent',
address: '0x1111...', // Agent's wallet (for tracking)
spendingLimit: parseUnits('100', 6), // Max $100 per transaction
dailyLimit: parseUnits('300', 6), // Max $300 per day
requiresApproval: parseUnits('50', 6) // Auto-approve under $50
});

coordinator.registerAgent({
id: 'data-agent',
name: 'Data Acquisition Agent',
address: '0x2222...',
spendingLimit: parseUnits('200', 6),
dailyLimit: parseUnits('500', 6),
requiresApproval: parseUnits('100', 6)
});

coordinator.registerAgent({
id: 'compute-agent',
name: 'Compute Agent',
address: '0x3333...',
spendingLimit: parseUnits('500', 6),
dailyLimit: parseUnits('1000', 6),
requiresApproval: parseUnits('200', 6)
});

// Create budgeted agents
const researchAgent = new BudgetedAgent(
'research-agent',
coordinator,
coordinatorClient
);

const dataAgent = new BudgetedAgent(
'data-agent',
coordinator,
coordinatorClient
);

// Simulate agent activities
console.log('\n--- Research Agent purchasing API access ---');
await researchAgent.purchaseService(
'0xAPIProvider...',
parseUnits('25', 6), // $25 - auto-approved
'Academic paper API access'
);

console.log('\n--- Data Agent purchasing dataset ---');
await dataAgent.purchaseService(
'0xDataProvider...',
parseUnits('150', 6), // $150 - requires approval
'Training dataset purchase'
);

// Print spending report
console.log('\n--- Budget Report ---');
const report = coordinator.getReport();
console.log(`Total Budget: ${formatUnits(report.totalBudget, 6)} USDC`);
console.log(`Total Spent: ${formatUnits(report.totalSpent, 6)} USDC`);
console.log(`Remaining: ${formatUnits(report.remaining, 6)} USDC`);
console.log('By Agent:', report.byAgent);
console.log('Pending Approvals:', report.pendingApprovals);
}

main().catch(console.error);

How It Worksโ€‹

ComponentPurposeExample
Treasury WalletSingle source of fundsCoordinator holds $1000
Per-Transaction LimitHard cap per spendMax $100 per transaction
Daily LimitPrevents runaway spendingMax $300 per day
Approval ThresholdHuman/senior reviewFlag purchases > $50
Spending RecordsAudit trailWho, what, when, why

Centralized Treasuryโ€‹

Why One Wallet?

All funds live in the coordinator's wallet:

  • Single source of truth - No fragmented balances
  • Easy auditing - All spending in one place
  • Simple recovery - One key to secure

Four-Level Authorizationโ€‹

Authorization Flow

Spending Recordsโ€‹

Every transaction is recorded for auditing:

{
agentId: 'research-agent',
amount: 25000000n, // $25 USDC
txId: '0xabc...',
timestamp: 1699876543,
provider: '0xAPIProvider...',
purpose: 'Academic paper API access'
}

Customization Pointsโ€‹

Role-Based Limitsโ€‹

Role-Based Spending Limits
type AgentRole = 'junior' | 'senior' | 'admin';

function getLimitsForRole(role: AgentRole): AgentLimits {
switch (role) {
case 'junior':
return {
spendingLimit: parseUnits('50', 6),
dailyLimit: parseUnits('100', 6),
requiresApproval: parseUnits('25', 6)
};
case 'senior':
return {
spendingLimit: parseUnits('500', 6),
dailyLimit: parseUnits('1000', 6),
requiresApproval: parseUnits('200', 6)
};
case 'admin':
return {
spendingLimit: parseUnits('10000', 6),
dailyLimit: parseUnits('50000', 6),
requiresApproval: parseUnits('5000', 6)
};
}
}

Provider Whitelistโ€‹

private providerWhitelist: Set<string> = new Set([
'0xTrustedProvider1...',
'0xTrustedProvider2...'
]);

async requestSpending(request: SpendingRequest): Promise<SpendingResponse> {
// Check provider is whitelisted
if (!this.providerWhitelist.has(request.provider.toLowerCase())) {
return {
approved: false,
reason: 'Provider not whitelisted'
};
}
// ... rest of checks
}

Spending Categoriesโ€‹

Spending Categories
interface SpendingRequest {
agentId: string;
amount: bigint;
provider: string;
purpose: string;
category: 'data' | 'compute' | 'api' | 'other';
}

// Category-specific budgets
private categoryBudgets = {
data: parseUnits('300', 6),
compute: parseUnits('500', 6),
api: parseUnits('200', 6),
other: parseUnits('100', 6)
};

private getCategorySpending(category: string): bigint {
return this.spending
.filter(s => s.category === category)
.reduce((sum, s) => sum + s.amount, 0n);
}

Gotchasโ€‹

Common Pitfalls

These are mistakes we made so you don't have to.

GotchaProblemSolution
Race conditionsTwo agents approve same $500 when only $600 leftUse mutex/lock for spending decisions
Partial failuresTransaction created but funding failsCancel tx if funding fails, don't record spend
In-memory stateServer restarts lose all spending recordsPersist to database
Stale daily limitsDaily limit never resetsImplement budget refresh cycle
Treasury key exposureCoordinator key leaked = all funds goneUse HSM or multisig

Race Conditionsโ€‹

// Use a mutex/lock for spending decisions
import { Mutex } from 'async-mutex';

private spendingMutex = new Mutex();

async requestSpending(request: SpendingRequest): Promise<SpendingResponse> {
return await this.spendingMutex.runExclusive(async () => {
// All spending checks and execution here
});
}

Failed Transactionsโ€‹

try {
const txId = await this.client.advanced.createTransaction({...});

try {
await this.client.escrow.fund(txId);
} catch (fundError) {
// Transaction created but not funded - CANCEL IT
console.error('Funding failed, cancelling transaction');
await this.client.advanced.transitionState(txId, State.CANCELLED, '0x');
throw fundError;
}

// Only record if fully successful
this.spending.push({...});

} catch (error) {
// Handle appropriately
}

Budget Refreshโ€‹

private lastReset: number = Date.now();
private resetInterval: number = 24 * 60 * 60 * 1000; // Daily

private checkBudgetReset(): void {
if (Date.now() - this.lastReset > this.resetInterval) {
this.archivedSpending.push(...this.spending);
this.spending = [];
this.lastReset = Date.now();
console.log('Budget reset for new period');
}
}

Production Checklistโ€‹

Data Persistenceโ€‹

  • Spending records in database (PostgreSQL, MongoDB)
  • Recovery mechanism for failed transactions
  • Audit log for all spending decisions

Concurrencyโ€‹

  • Mutex/lock for spending decisions
  • Idempotency keys for retry safety

Monitoringโ€‹

  • Alerting at 80%, 90%, 100% budget thresholds
  • Dashboard for real-time spending
  • Slack/Discord notifications for approvals

Securityโ€‹

  • Treasury key in HSM or secrets manager
  • Emergency stop capability
  • Multi-sig for large approvals
Start With Memory

For testing, in-memory is fine. Persist to database when you're handling real money.


Next Stepsโ€‹

๐Ÿ” Secure Keys

Protect that treasury wallet.

Key Management โ†’

๐Ÿค– Provider Agent

Build the other side of the market.

Provider Agent โ†’

๐Ÿ“š Consumer Guide

Deep dive on consuming services.

Consumer Agent โ†’