Building a Bulletproof AI Agent Billing System: Step-by-Step
A complete implementation guide for building an AI agent billing system that handles micropayments, enforces budgets, logs transactions, and scales to millions of agent calls.
Every AI agent billing system starts with good intentions and ends with a spreadsheet tracking $12,000 in unexplained API costs. The gap between “we’ll add billing later” and “we need billing now” is measured in invoice shock.
This guide walks you through building a production-grade AI agent billing system from scratch. Not a prototype — a system that handles micropayments, enforces budgets, logs every transaction, and scales to millions of agent calls without breaking.
Architecture Overview
Before writing code, understand the four layers of an agent billing system:
┌─────────────────────────────────────────────────┐
│ Agent Runtime │
│ (your agent code that calls paid APIs) │
├─────────────────────────────────────────────────┤
│ Budget Policy Engine │
│ (evaluates spending limits before each call) │
├─────────────────────────────────────────────────┤
│ Payment Rail │
│ (x402, Stripe, USDC — processes the payment) │
├─────────────────────────────────────────────────┤
│ Audit & Logging │
│ (records every transaction for analysis) │
└─────────────────────────────────────────────────┘
Each layer is independent. You can swap payment rails without changing budget policies. You can change logging without affecting payment processing. This separation is what makes the system bulletproof.
Step 1: Define Your Billing Data Model
Start with the data model. Every transaction needs enough context to answer three questions: Who spent it? What did they buy? Was it authorized?
Transaction Schema
interface AgentTransaction {
id: string; // unique transaction ID
agentId: string; // which agent made the call
taskId: string; // what the agent was trying to do
endpoint: string; // which API was called
amount: number; // cost in USD
currency: string; // USD, USDC, etc.
paymentRail: string; // x402, stripe, usdc
status: 'approved' | 'blocked' | 'escalated' | 'pending';
policy: {
rule: string; // which rule was evaluated
remaining: number; // budget remaining after this call
};
timestamp: number; // Unix timestamp
metadata: Record<string, any>; // additional context
}
Budget Policy Schema
interface BudgetPolicy {
agentId: string; // agent identifier (or '*' for all)
limits: {
perCall: number; // max cost per single call
perAgent: {
daily: number; // max daily spend per agent
monthly: number; // max monthly spend per agent
};
perEndpoint: {
daily: number; // max daily spend per endpoint
};
perTool: {
hourly: number; // max hourly spend per tool
};
};
escalation: {
threshold: number; // alert when spending reaches % of limit
channel: string; // notification channel (Slack, email, etc.)
};
}
This data model is the foundation. Everything else builds on it.
Step 2: Implement Budget Policy Enforcement
Budget policies must be evaluated before each payment is processed. This is the critical difference between a billing system and a cost control system.
Policy Evaluation Engine
import { defineBudget } from '@harpd/agent-budget-policy';
class BudgetEnforcer {
private policies: Map<string, BudgetPolicy> = new Map();
private spending: Map<string, number> = new Map();
constructor() {
// Load policies from configuration
this.loadPolicies();
}
async evaluate(
agentId: string,
endpoint: string,
amount: number
): Promise<{ approved: boolean; reason?: string; remaining?: number }> {
const policy = this.getPolicy(agentId);
if (!policy) {
return { approved: false, reason: 'no_policy_found' };
}
// Check per-call limit
if (amount > policy.limits.perCall) {
return {
approved: false,
reason: `per_call_limit_exceeded: ${amount} > ${policy.limits.perCall}`,
};
}
// Check daily agent spending
const dailyKey = `agent:${agentId}:daily:${this.today()}`;
const dailySpent = this.spending.get(dailyKey) || 0;
if (dailySpent + amount > policy.limits.perAgent.daily) {
return {
approved: false,
reason: `daily_agent_limit_exceeded: ${dailySpent + amount} > ${policy.limits.perAgent.daily}`,
remaining: policy.limits.perAgent.daily - dailySpent,
};
}
// Check endpoint daily limit
const endpointKey = `endpoint:${endpoint}:daily:${this.today()}`;
const endpointSpent = this.spending.get(endpointKey) || 0;
if (endpointSpent + amount > policy.limits.perEndpoint.daily) {
return {
approved: false,
reason: `daily_endpoint_limit_exceeded`,
remaining: policy.limits.perEndpoint.daily - endpointSpent,
};
}
// All checks passed
return {
approved: true,
remaining: policy.limits.perAgent.daily - dailySpent - amount,
};
}
async recordSpend(agentId: string, endpoint: string, amount: number) {
const dailyKey = `agent:${agentId}:daily:${this.today()}`;
const endpointKey = `endpoint:${endpoint}:daily:${this.today()}`;
this.spending.set(dailyKey, (this.spending.get(dailyKey) || 0) + amount);
this.spending.set(endpointKey, (this.spending.get(endpointKey) || 0) + amount);
}
}
Integration with Agent Runtime
// In your agent's API call handler
async function callPaidEndpoint(agentId: string, task: string, endpoint: string) {
const enforcer = new BudgetEnforcer();
// Step 1: Check budget before calling
const check = await enforcer.evaluate(agentId, endpoint, estimatedCost);
if (!check.approved) {
// Log the blocked transaction
await logTransaction({
agentId,
task,
endpoint,
amount: estimatedCost,
status: 'blocked',
reason: check.reason,
});
// Return structured error to agent
return {
error: 'budget_exceeded',
reason: check.reason,
remaining: check.remaining,
retryAfter: getNextWindow(),
};
}
// Step 2: Call the paid endpoint
const result = await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({ task }),
});
// Step 3: Record the spend
await enforcer.recordSpend(agentId, endpoint, actualCost);
// Step 4: Log the approved transaction
await logTransaction({
agentId,
task,
endpoint,
amount: actualCost,
status: 'approved',
});
return result;
}
The key insight: budget checks happen before the API call, not after. This prevents overspending by design, not by monitoring.
Step 3: Implement Payment Processing
With budget enforcement in place, you need a payment rail. For AI agent micropayments, x402 is the optimal choice.
x402 Integration
import { x402Logger } from '@harpd/x402-logging-middleware';
// Configure x402 payment rail
const paymentConfig = {
destination: 'your-log-sink', // where to write transaction logs
includeMetadata: true, // include agent context in logs
};
// Add x402 middleware to your API
app.use(x402Logger(paymentConfig));
// Define a paid endpoint
app.post('/api/extract', async (req, res) => {
// x402 handles payment automatically
// Agent pays $0.01 per call via x402 protocol
const result = await extractData(req.body);
res.json(result);
});
USDC Settlement (Alternative)
If you prefer USDC settlement over x402:
import { HarpdSettlement } from '@harpd/observe';
const settlement = new HarpdSettlement({
apiKey: process.env.HARPD_KEY,
network: 'base', // Ethereum L2 for low fees
});
async function processPayment(agentId: string, amount: number) {
const tx = await settlement.pay({
from: agentWallet,
to: merchantWallet,
amount: amount,
currency: 'USDC',
metadata: { agentId, task: currentTask },
});
return tx;
}
Both approaches work. x402 is simpler for API monetization. USDC settlement is more flexible for agent-to-agent payments.
Step 4: Build the Audit Trail
An audit trail is not a nice-to-have — it’s a requirement for enterprise customers and compliance. Every transaction must be logged with enough context to reconstruct the agent’s behavior.
Transaction Logger
import { x402Logger } from '@harpd/x402-logging-middleware';
interface TransactionLog {
id: string;
agentId: string;
taskId: string;
endpoint: string;
amount: number;
status: 'approved' | 'blocked' | 'escalated' | 'pending';
paymentRail: string;
timestamp: number;
metadata: Record<string, any>;
}
class TransactionLogger {
private buffer: TransactionLog[] = [];
private flushInterval: NodeJS.Timer;
constructor(private sink: LogSink) {
// Flush buffer every 5 seconds
this.flushInterval = setInterval(() => this.flush(), 5000);
}
async log(transaction: TransactionLog) {
this.buffer.push(transaction);
// Immediate flush for blocked/escalated transactions
if (transaction.status !== 'approved') {
await this.flush();
}
}
private async flush() {
if (this.buffer.length === 0) return;
const batch = [...this.buffer];
this.buffer = [];
await this.sink.write(batch);
}
}
// Usage
const logger = new TransactionLogger(new DatabaseSink());
await logger.log({
id: generateId(),
agentId: 'research-bot',
taskId: 'analyze-competitor-pricing',
endpoint: '/api/extract',
amount: 0.01,
status: 'approved',
paymentRail: 'x402',
timestamp: Date.now(),
metadata: {
model: 'claude-3-haiku',
tokens: 1250,
latency: 340,
},
});
Query Interface
The audit trail needs to be queryable. Build a simple query interface:
class AuditQuery {
async getTransactionsByAgent(agentId: string, dateRange: DateRange) {
return db.query(
'SELECT * FROM transactions WHERE agent_id = ? AND timestamp BETWEEN ? AND ?',
[agentId, dateRange.start, dateRange.end]
);
}
async getDailySpendByAgent(date: string) {
return db.query(
'SELECT agent_id, SUM(amount) as total FROM transactions WHERE DATE(timestamp) = ? GROUP BY agent_id',
[date]
);
}
async getBlockedTransactions(dateRange: DateRange) {
return db.query(
'SELECT * FROM transactions WHERE status = ? AND timestamp BETWEEN ? AND ?',
['blocked', dateRange.start, dateRange.end]
);
}
}
This query interface answers the questions finance teams ask: Who spent what? When? Was it authorized?
Step 5: Add Human Escalation
No budget policy can anticipate every scenario. When an agent hits an unusual pattern, the system should pause and notify a human.
Escalation Rules
interface EscalationRule {
condition: (transaction: TransactionLog, spending: SpendingSummary) => boolean;
action: 'notify' | 'block' | 'pause_agent';
channel: string;
message: string;
}
const escalationRules: EscalationRule[] = [
{
// Alert when daily spend exceeds 80% of budget
condition: (tx, spending) =>
spending.dailyTotal / spending.dailyBudget > 0.8,
action: 'notify',
channel: '#finance-alerts',
message: 'Agent {{agentId}} has spent {{percentage}} of daily budget',
},
{
// Block when daily spend exceeds 100% of budget
condition: (tx, spending) =>
spending.dailyTotal >= spending.dailyBudget,
action: 'block',
channel: '#finance-alerts',
message: 'Agent {{agentId}} exceeded daily budget - transactions blocked',
},
{
// Pause agent when hourly spend spikes 5x above average
condition: (tx, spending) =>
spending.hourlySpend > spending.averageHourlySpend * 5,
action: 'pause_agent',
channel: '#security-alerts',
message: 'Agent {{agentId}} spending spike detected - agent paused',
},
];
Escalation Handler
class EscalationHandler {
constructor(
private notifier: NotificationService,
private agentController: AgentController
) {}
async evaluate(transaction: TransactionLog, spending: SpendingSummary) {
for (const rule of escalationRules) {
if (rule.condition(transaction, spending)) {
await this.execute(rule, transaction, spending);
}
}
}
private async execute(
rule: EscalationRule,
transaction: TransactionLog,
spending: SpendingSummary
) {
const message = this.interpolate(rule.message, {
agentId: transaction.agentId,
percentage: ((spending.dailyTotal / spending.dailyBudget) * 100).toFixed(1),
});
switch (rule.action) {
case 'notify':
await this.notifier.send(rule.channel, message);
break;
case 'block':
await this.notifier.send(rule.channel, message);
// Block future transactions for this agent
await budgetEnforcer.blockAgent(transaction.agentId);
break;
case 'pause_agent':
await this.notifier.send(rule.channel, message);
// Pause the agent entirely
await this.agentController.pause(transaction.agentId);
break;
}
}
}
Escalation is the safety net. It catches what budget policies miss and prevents incidents from becoming disasters.
Step 6: Build the Monitoring Dashboard
You can’t optimize what you can’t measure. Build a real-time dashboard that shows:
Key Metrics
interface BillingMetrics {
// Volume metrics
totalTransactions: number;
transactionsPerMinute: number;
averageTransactionsPerAgent: number;
// Cost metrics
totalSpend: number;
averageCostPerTransaction: number;
costByAgent: Map<string, number>;
costByEndpoint: Map<string, number>;
// Health metrics
blockedTransactions: number;
escalationCount: number;
budgetUtilization: Map<string, number>;
// Performance metrics
averageLatency: number;
p99Latency: number;
errorRate: number;
}
Dashboard Queries
class BillingDashboard {
async getHourlySpend() {
return db.query(`
SELECT
DATE_TRUNC('hour', timestamp) as hour,
SUM(amount) as total_spend,
COUNT(*) as transaction_count,
COUNT(CASE WHEN status = 'blocked' THEN 1 END) as blocked_count
FROM transactions
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY DATE_TRUNC('hour', timestamp)
ORDER BY hour DESC
`);
}
async getTopAgentsBySpend(limit: number = 10) {
return db.query(`
SELECT
agent_id,
SUM(amount) as total_spend,
COUNT(*) as transaction_count,
AVG(amount) as avg_cost_per_call
FROM transactions
WHERE timestamp > NOW() - INTERVAL '30 days'
GROUP BY agent_id
ORDER BY total_spend DESC
LIMIT ?
`, [limit]);
}
async getBudgetUtilization() {
return db.query(`
SELECT
agent_id,
SUM(amount) as spent,
policy.daily as budget,
(SUM(amount) / policy.daily * 100) as utilization_pct
FROM transactions
JOIN policies ON transactions.agent_id = policies.agent_id
WHERE DATE(timestamp) = CURRENT_DATE
GROUP BY agent_id, policy.daily
`);
}
}
This dashboard turns billing data into actionable insights. When spending spikes, you see it immediately. When budgets are nearly exhausted, you get alerts before they’re hit.
Step 7: Handle Edge Cases
Production billing systems face edge cases that prototypes don’t. Here’s how to handle the most common ones:
Concurrent Agent Spending
When multiple agents share a budget, race conditions can cause overspending:
// Use atomic operations for budget tracking
async function recordSpendAtomic(agentId: string, amount: number) {
const key = `budget:${agentId}:daily:${today()}`;
// Atomic increment with limit check
const result = await redis.eval(`
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
local limit = tonumber(ARGV[1])
local amount = tonumber(ARGV[2])
if current + amount > limit then
return {err = 'budget_exceeded'}
end
redis.call('INCRBY', KEYS[1], amount * 1000000)
return {ok = true, remaining = limit - current - amount}
`, 1, key, dailyBudget.toString(), amount.toString());
return result;
}
Payment Failures
When a payment fails, the agent should handle it gracefully:
async function handlePaymentFailure(agentId: string, error: PaymentError) {
// Log the failure
await logTransaction({
agentId,
status: 'failed',
error: error.message,
retryable: error.retryable,
});
// If retryable, let the agent retry with backoff
if (error.retryable) {
return {
error: 'payment_failed',
retryable: true,
retryAfter: calculateBackoff(error.attempt),
};
}
// If not retryable, escalate to human
await escalate({
agentId,
reason: 'payment_failure_permanent',
error: error.message,
});
return {
error: 'payment_failed',
retryable: false,
escalationId: await createEscalation(agentId, error),
};
}
Budget Reset Timing
Budgets need to reset at consistent times, but timezone handling is tricky:
function getNextResetTime(timezone: string): Date {
const now = new Date();
const localNow = new Date(now.toLocaleString('en-US', { timeZone: timezone }));
// Reset at midnight local time
const resetTime = new Date(localNow);
resetTime.setHours(24, 0, 0, 0);
// Convert back to UTC
const utcReset = new Date(resetTime.toLocaleString('en-US', { timeZone: 'UTC' }));
return utcReset;
}
These edge cases are where billing systems fail in production. Handle them upfront, or they’ll handle you later.
Step 8: Scale to Millions of Transactions
When your billing system handles 10 transactions per day, everything works. When it handles 10 million, you need to think about scale.
Horizontal Scaling
// Use a message queue for transaction processing
const queue = new BullMQ('billing-queue', {
connection: redis,
defaultJobOptions: {
removeOnComplete: 1000,
removeOnFail: 5000,
},
});
// Producer: queue transactions
async function queueTransaction(transaction: TransactionLog) {
await queue.add('process-transaction', transaction, {
priority: transaction.status === 'blocked' ? 1 : 0,
});
}
// Consumer: process transactions
const worker = new Worker('billing-queue', async (job) => {
const transaction = job.data;
// Write to database
await db.insert('transactions', transaction);
// Update real-time metrics
await metrics.increment('transactions.processed');
await metrics.increment(`transactions.${transaction.status}`);
await metrics.histogram('transaction.amount', transaction.amount);
// Check escalation rules
await escalationHandler.evaluate(transaction, await getSpendingSummary(transaction.agentId));
}, {
connection: redis,
concurrency: 100, // process 100 transactions in parallel
});
Database Optimization
// Partition tables by time for fast queries
await db.query(`
CREATE TABLE transactions (
id UUID PRIMARY KEY,
agent_id TEXT NOT NULL,
amount DECIMAL(10,6) NOT NULL,
status TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (timestamp)
`);
// Create partitions for each month
await db.query(`
CREATE TABLE transactions_2026_09 PARTITION OF transactions
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01')
`);
// Add indexes for common queries
await db.query(`
CREATE INDEX idx_transactions_agent_date
ON transactions (agent_id, timestamp DESC)
`);
await db.query(`
CREATE INDEX idx_transactions_status_date
ON transactions (status, timestamp DESC)
`);
Caching for Hot Paths
// Cache budget utilization to avoid repeated queries
class BudgetCache {
private cache = new LRUCache<string, number>({
max: 10000,
ttl: 30_000, // 30 second TTL
});
async getSpent(agentId: string): Promise<number> {
const key = `spent:${agentId}:${today()}`;
let spent = this.cache.get(key);
if (spent === undefined) {
spent = await db.getDailySpend(agentId);
this.cache.set(key, spent);
}
return spent!;
}
async increment(agentId: string, amount: number): Promise<number> {
const key = `spent:${agentId}:${today()}`;
const newSpent = (this.cache.get(key) || 0) + amount;
this.cache.set(key, newSpent);
return newSpent;
}
}
At scale, the billing system becomes a high-throughput data pipeline. Design it as one from the start.
The Complete System
Putting it all together, here’s the complete architecture:
// Initialize all components
const budgetEnforcer = new BudgetEnforcer();
const paymentProcessor = new PaymentProcessor();
const transactionLogger = new TransactionLogger();
const escalationHandler = new EscalationHandler();
const dashboard = new BillingDashboard();
// Main billing flow
async function processAgentPayment(
agentId: string,
task: string,
endpoint: string
): Promise<PaymentResult> {
const transactionId = generateId();
try {
// Step 1: Check budget
const budgetCheck = await budgetEnforcer.evaluate(agentId, endpoint, estimatedCost);
if (!budgetCheck.approved) {
await transactionLogger.log({
id: transactionId,
agentId,
task,
endpoint,
amount: estimatedCost,
status: 'blocked',
reason: budgetCheck.reason,
timestamp: Date.now(),
});
return { success: false, reason: budgetCheck.reason };
}
// Step 2: Process payment
const payment = await paymentProcessor.charge({
agentId,
amount: estimatedCost,
endpoint,
});
if (!payment.success) {
await transactionLogger.log({
id: transactionId,
agentId,
task,
endpoint,
amount: estimatedCost,
status: 'failed',
error: payment.error,
timestamp: Date.now(),
});
return { success: false, reason: payment.error };
}
// Step 3: Record spend
await budgetEnforcer.recordSpend(agentId, endpoint, estimatedCost);
// Step 4: Log successful transaction
await transactionLogger.log({
id: transactionId,
agentId,
task,
endpoint,
amount: estimatedCost,
status: 'approved',
paymentId: payment.id,
timestamp: Date.now(),
});
// Step 5: Check escalation rules
const spendingSummary = await budgetEnforcer.getSpendingSummary(agentId);
await escalationHandler.evaluate(
{ id: transactionId, agentId, amount: estimatedCost, status: 'approved' } as any,
spendingSummary
);
return { success: true, transactionId };
} catch (error) {
// Step 6: Handle unexpected errors
await transactionLogger.log({
id: transactionId,
agentId,
task,
endpoint,
amount: estimatedCost,
status: 'error',
error: error.message,
timestamp: Date.now(),
});
throw error;
}
}
What You Get
A bulletproof AI agent billing system provides:
- Cost control — budget policies prevent overspending by design
- Audit compliance — every transaction is logged with full context
- Real-time visibility — dashboards show spending as it happens
- Graceful degradation — failures are handled, not ignored
- Scalability — the system grows with your agent traffic
The investment in building this system pays for itself the first time it prevents a runaway agent from burning through your API budget.
Ready to build your agent billing system? Harpd provides the open-source tools — agent budget policies, x402 logging, and paid MCP tools — to get started in under an hour.