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

> Discover how Paperclip tracks and aggregates cost events across agents, tasks, and projects. Learn about centralized LLM billing records and data aggregation.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: architecture
- Published: 2026-08-16

---

**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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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 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`](https://github.com/paperclipai/paperclip/blob/main/ui/src/pages/IssueDetail.tsx), where issue-level costs sum across all related runs:

```typescript
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`](https://github.com/paperclipai/paperclip/blob/main/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:

```typescript
// 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:

```typescript
// 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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/ui/src/pages/StatusCards/format.ts):

```typescript
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`](https://github.com/paperclipai/paperclip/blob/main/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:

```typescript
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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/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.