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:

  1. Cost control — budget policies prevent overspending by design
  2. Audit compliance — every transaction is logged with full context
  3. Real-time visibility — dashboards show spending as it happens
  4. Graceful degradation — failures are handled, not ignored
  5. 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.