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.
How Cost Events Link to Agents, Tasks, and Projects
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 agentheartbeat_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 ticketproject_id— rolls up to a containing projectgoal_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.tsare the single source of truth for LLM spending in Paperclip - Foreign key relationships to
agent_id,heartbeat_run_id,issue_id,project_id, andgoal_idenable flexible roll-up dimensions - Three rolling windows (5h, 24h, 7d) with provider grouping power operational dashboards
- Live streaming via
LiveUpdatesProviderdelivers 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →