How Paperclip Tracks and Aggregates Cost Events Across Agents, Tasks, and Projects

Paperclip records every LLM usage billing record as a cost event in a centralized database table, then aggregates these events by time windows and organizational hierarchies through a dedicated Cost Service.

The paperclipai/paperclip open-source repository implements a comprehensive cost tracking system that gives teams real-time visibility into LLM spending. This article explains how the system captures, stores, and rolls up cost data from individual API calls to project-level dashboards.


What Is a Cost Event in Paperclip?

A cost event is the atomic unit of billing data in Paperclip. Each time an agent makes an LLM request, the runtime creates a row in the cost_events table defined in packages/db/src/schema/cost_events.ts.

The schema captures eight core dimensions:

Column Purpose
company_id Organization that owns the run
agent_id Agent that initiated the request (nullable)
heartbeat_run_id Specific heartbeat execution that generated the call (nullable)
issue_id / project_id / goal_id Optional links to work tracking entities
provider / biller LLM provider (e.g., openai, anthropic) and billing source
cost_cents Charged amount as integer cents
input_tokens, output_tokens Token consumption including cached tokens
occurred_at Precise timestamp for the event

Foreign key relationships tie cost events to the broader finance ledger via packages/db/src/schema/finance_events.ts, enabling audit trails and accounting reconciliation.


Paperclip uses nullable foreign keys to create a flexible hierarchy of cost attribution. This design lets the same event belong to multiple organizational contexts without redundant storage.

Agents and Heartbeat Runs

Every cost event can reference:

  • agent_id — ties the spend to a specific autonomous agent
  • heartbeat_run_id — groups costs by the periodic refresh cycle that triggered the LLM call

This dual linkage enables both agent-centric budgeting and run-level debugging. When a heartbeat runs every few minutes to check task status, all its LLM calls roll up to that specific run instance.

Issues, Projects, and Goals

Optional foreign keys support work-item attribution:

  • issue_id — links to a specific task or bug ticket
  • project_id — rolls up to a containing project
  • goal_id — associates with higher-level objectives

The UI leverages these links in ui/src/pages/IssueDetail.tsx, where issue-level costs sum across all related runs:

let cost = 0;
for (const run of runs) {
  cost += run.cost.cents;
}

Cost Aggregation Architecture

Raw cost events feed into rolling aggregations via the server-side Cost Service at server/src/services/costs.ts. The service computes three standard time windows:

  • 5 hours — near-real-time operational visibility
  • 24 hours — daily spend tracking
  • 7 days — weekly budget monitoring

Aggregation Query Pattern

The core aggregation uses Drizzle ORM with SQL windowing:

// server/src/services/costs.ts
const result = await db
  .select({
    provider: costEvents.provider,
    windowHours: sql<number>`5`,
    totalCostCents: sql<number>`SUM(${costEvents.costCents})`,
  })
  .from(costEvents)
  .where(costEvents.occurredAt.gte(windowStart))
  .groupBy(costEvents.provider);

Results group by provider to surface spend distribution across OpenAI, Anthropic, and other LLM vendors.

Project-Level Aggregation

The getProjectCostSummary function extends this pattern with additional filters:

// server/src/services/costs.ts – getProjectCostSummary
const summary = await db
  .select({
    provider: costEvents.provider,
    totalCostCents: sql<number>`SUM(${costEvents.costCents})`,
  })
  .from(costEvents)
  .where(and(
    eq(costEvents.projectId, projectId),
    gte(costEvents.occurredAt, windowStart),
  ))
  .groupBy(costEvents.provider);

Real-Time Cost Streaming to the UI

Paperclip's frontend receives live cost updates through the LiveUpdatesProvider in ui/src/context/LiveUpdatesProvider.tsx. This component subscribes to the "cost_event" entity type, pushing new rows to dashboards without page refresh.

Components like ProviderQuotaCard and AccountingModelCard consume these updates to display:

  • Current spend versus subscription limits
  • Provider-specific usage breakdowns
  • Token consumption rates

Cost Formatting Utilities

Raw integer cents convert to human-readable strings via ui/src/pages/StatusCards/format.ts:

import { formatCents, formatTokens } from "./format";

const costLabel = formatCents(card.todayCostCents);
const tokenLabel = formatTokens(card.todayInputTokens + card.todayOutputTokens);

// Produces: "$0.48 · 96k tok"
return <span>{costLabel && `· ${costLabel}`} {tokenLabel && `· ${tokenLabel}`}</span>;

REST API Endpoints for Cost Data

The server/src/routes/costs.ts module exposes aggregated cost data through standard endpoints:

Endpoint Purpose
GET /api/issues/:id/costs Per-provider cost summary for a specific issue
GET /api/projects/:id/costs Project-level rollup with time window filters
GET /api/agents/:id/costs Agent-specific spend analysis

These endpoints power both the web UI and external integrations, returning JSON with totalCostCents, provider breakdowns, and windowHours scope.


Creating Cost Events at Runtime

When an LLM request completes, the runtime persists billing data through a standard insert pattern:

await db.insert(costEvents).values({
  companyId: company.id,
  agentId: agent?.id,
  heartbeatRunId: run.id,
  provider: "openai",
  biller: "openai",
  costCents: usage.costCents,
  inputTokens: usage.inputTokens,
  outputTokens: usage.outputTokens,
  occurredAt: new Date(),
});

The biller field supports future multi-tenant billing scenarios where a single company might use different payment instruments for different agent teams.


Summary

  • Cost events in packages/db/src/schema/cost_events.ts are the single source of truth for LLM spending in Paperclip
  • Foreign key relationships to agent_id, heartbeat_run_id, issue_id, project_id, and goal_id enable flexible roll-up dimensions
  • Three rolling windows (5h, 24h, 7d) with provider grouping power operational dashboards
  • Live streaming via LiveUpdatesProvider delivers real-time cost visibility without polling
  • Formatting utilities convert raw cents and tokens into readable labels like "$0.48 · 96k tok"

Frequently Asked Questions

How does Paperclip handle cost events when an agent runs without a specific issue or project?

The issue_id, project_id, and goal_id columns are nullable, so cost events still record with NULL values for these dimensions. Aggregation queries typically filter by the available context—for example, agent_id for agent-level reporting—while unassigned costs appear in company-wide totals. The schema in packages/db/src/schema/cost_events.ts reflects this permissive design.

What database indexes support cost aggregation performance?

The cost_events table includes composite indexes on (company_id, occurred_at) and (project_id, occurred_at) to accelerate time-windowed aggregations. The Drizzle schema definition in packages/db/src/schema/cost_events.ts declares these indexes alongside the table structure.

How does Paperclip distinguish between different LLM providers in cost reports?

Each cost event stores both provider (the API vendor like openai or anthropic) and biller (the payment source). The server/src/services/costs.ts aggregation groups explicitly by costEvents.provider, enabling per-vendor spending breakdowns. The UI components display these as labeled segments in quota cards.

Can external systems query Paperclip's cost data?

Yes. The REST endpoints in server/src/routes/costs.ts expose JSON cost summaries for issues, projects, and agents. These endpoints use the same authentication middleware as other API routes, allowing programmatic access with appropriate credentials.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →