How I Built a Unified Cost Tracker for AI Coding Agents (Claude Code, Cursor, Codex) — Tutorial

This tutorial shows how to build a local, unified cost tracker that reads JSONL usage logs from Claude Code, Cursor and Codex, extracts token usage regardless of field shape, estimates costs using a multi-provider pricing table (including cache-read discounts), and enforces simple monthly budget guardrails. It targets developers who keep agent logs on-disk and want a privacy-first, local reporting tool.

Key takeaways

  • Use a recursive extractor to handle multiple and changing JSON shapes instead of brittle per-provider parsers.
  • Apply a cache-read discount (example: ~10x cheaper) to avoid overstating costs for cached reads.
  • Keep a broad pricing table and fall back to an estimated rate for unknown models, and clearly mark estimates in reports.

Prerequisites

Before you start, you should have:

  • Node.js and npm installed (the example tooling in this tutorial is JavaScript/Node-oriented).
  • Local JSONL log files from the agents you use (Claude Code, Cursor, Codex or similar). Each line should be a JSON object; field names vary by agent.
  • Basic familiarity with reading and writing JSON and with the command line.

Design goals

Keep these goals in mind while implementing the tracker:

  • Local-first: do not upload logs or usage to remote services.
  • One parser to handle multiple, changing JSON shapes rather than brittle per-provider parsers.
  • Practical pricing: support many providers and fall back with an estimated rate for unknown models so totals never silently omit cost.
  • Cache-read accounting: treat cached reads as substantially cheaper than fresh input (typical ~10x cheaper) to avoid overstating costs.
  • Budget guardrails: warn at a configurable threshold (example: 80%) and flag at 100% of monthly budget.

Setup

Create a new npm project and install any helper libraries you’d like (this tutorial assumes plain Node.js without external parsing libs):

mkdir agent-cost && cd agent-cost
npm init -y

Create a simple project layout:

.
├─ logs/           # put JSONL agent logs here
├─ src/
│  ├─ extract.js   # recursive extractor
│  ├─ pricing.js   # pricing engine and lookup
│  ├─ scan.js      # directory scanner and report generator
│  └─ budget.js    # budget guardrail logic
└─ package.json

Step 1 — Recursive usage extractor

Problem: each agent uses different field names and nesting. Solution: walk the object tree, find any object that has a usage field, and normalize token counts with fallbacks. The extractor should be tolerant of shapes such as:

  • Claude Code: usage nested under message with fields like input_tokens, output_tokens, cache_read_input_tokens and a timestamp.
  • Codex CLI: usage nested under payload → message with input_tokens/output_tokens.
  • Cursor: usage may use prompt_tokens and completion_tokens.

Implement a recursive extractor that collects normalized entries. Each entry should include model, inputTokens, outputTokens, cachedReadTokens and a timestamp where available.

// pseudocode-style snippet (keep short and focused)
function extractUsage(obj, out, parentTs) {
  if (!obj || typeof obj !== 'object') return;
  if (obj.usage) {
    out.push({
      model: obj.model || (obj.message && obj.message.model) || '',
      inputTokens: obj.usage.input_tokens ?? obj.usage.prompt_tokens ?? 0,
      outputTokens: obj.usage.output_tokens ?? obj.usage.completion_tokens ?? 0,
      cachedReadTokens: obj.usage.cache_read_input_tokens ?? 0,
      ts: parentTs || obj.timestamp || ''
    });
    return;
  }
  for (const k of Object.keys(obj)) {
    if (['usage','message','payload','request'].includes(k)) {
      extractUsage(obj[k], out, obj.timestamp);
    }
  }
}

Notes:

  • Do not assume a single fixed key path. The recursive approach tolerates future agents and field-name drift.
  • Record the timestamp if present. If the parent object has a timestamp, pass it down to child extracts.

Step 2 — Directory scanner and JSONL reader

Scan a directory of JSONL files, read each line, parse JSON, and feed objects into the extractor. Produce a flattened array of usage entries for the reporting step.

  1. Open each file in the logs directory.
  2. Read line-by-line; skip empty lines and handle parse errors by logging the filename and line number (do not abort the whole run).
  3. Pass parsed objects to extractUsage and collect results.

Step 3 — Pricing engine with provider fallbacks

Maintain a pricing table mapping known model names or prefixes to input/output prices (price per 1,000 tokens). Include an estimated flag for fallback rates so users can see which items are approximations.

Key behavior:

  • Match model exactly or by prefix to pick the correct provider entry.
  • If a model is unknown, return an estimated rate rather than zero and mark the line as estimated.
  • Apply a cache-read discount: treat cachedReadTokens as priced at a fraction of fresh input (example: 0.1×).
// concise logic
function estimateCost({ model, inputTokens, outputTokens, cachedReadTokens = 0 }) {
  const price = lookupModel(model); // returns { input: $/1k, output: $/1k, estimated: bool }
  const cached = Number(cachedReadTokens) || 0;
  const cost = (inputTokens * price.input + cached * price.input * 0.1 + outputTokens * price.output) / 1000;
  return { cost, estimated: !!price.estimated };
}

Practical tips:

  • Support a broad provider list (the example project reuses a table covering many providers). You do not need perfect per-model accuracy to detect runaway costs; a reasonably accurate table plus fallbacks is far better than ignoring unknown models.
  • Keep the pricing table editable (JSON or compact JS object) so you can update rates as providers change them.

Step 4 — Aggregation and report

Aggregate the estimated costs by model, by day, and by agent or project tag if you can infer one. Produce a human-readable terminal report and a compact machine-readable summary for programmatic checks.

  • Daily totals: group entries by date derived from the timestamp.
  • Per-model totals: sum estimated cost and mark any contribution that used estimated rates.
  • Show cache-read contribution separately or folded into input with a note about discounting.

Example terminal output structure (conceptual):

Month total: $123.45
  2026-08-01: $12.34
  Models:
    claude-sonnet-4-20250514: $45.67 (includes estimated lines)
    gpt-5-codex: $30.00
Warnings:
  Budget at 82% of $150 monthly limit

Step 5 — Budget guardrails

Allow the user to set a monthly budget. The tool should compute the current period spend and compare it to the budget, emitting two levels of alerts:

  • Warning at a configurable threshold (example: 80% of the budget).
  • Flag when spend reaches or exceeds the budget (100%).

Implement simple functions to load and persist a budget (local JSON config) and to evaluate the current spend percentage. The report should clearly indicate when parts of the total are estimated so users know to investigate unknown models rather than assume accuracy.

Verification and tests

To verify correctness:

  1. Create small synthetic JSONL files that mirror the three example shapes described above and include edge cases (missing usage, zero tokens, cached read counts).
  2. Run the scanner and inspect the flattened usage entries to ensure fields map correctly.
  3. Compare manual cost calculations with the pricing engine’s output for a few samples, including cached reads priced at the lower rate.
  4. Add unit tests that cover extractor behavior, pricing fallback behavior, and budget threshold logic. The referenced project reports 22/22 tests passing in early release; aim for similar coverage in your own suite.

Troubleshooting

  • Parser finds no usage objects: confirm the JSONL lines are valid JSON and that the logs include a usage field somewhere in the object tree.
  • Many lines flagged as estimated: update the pricing table with recent model names or prefixes used by your agents.
  • Costs seem too high: check whether cachedReadTokens are being recognized and discounted; ensure you aren’t double-counting tokens from nested objects.
  • Large logs are slow to scan: process files streamingly (line by line) instead of loading whole files into memory.

Security and performance notes

  • Local-first: do not send logs or token counts to remote services if you require privacy. Keep both logs and config on disk with appropriate filesystem permissions.
  • Memory: use streaming file reads for large archives to keep memory usage bounded.
  • Accuracy vs usefulness: using an estimated rate for unknown models is intentionally conservative to avoid silently omitting cost. Surface “estimated” clearly so users can follow up and improve the pricing table.

A small local tool that normalizes diverse JSONL usage logs, uses a broad pricing table with cache-read discounts, and enforces simple budget guardrails gives immediate value: it turns fragmented, inconsistent agent logs into actionable spend insight without sending sensitive data off the machine. The recursive extractor pattern avoids brittle per-provider parsers, and estimated pricing ensures nothing is silently dropped from totals. From here you can add project-level budgets, team aggregation, anomaly alerts, or a minimal MCP interface so agents themselves can ask “how much have I spent this month?”

Stay in Touch

spot_img

Related Articles