DEV Community

justin li
justin li

Posted on • Originally published at sellertools.dev

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

How I Built a Unified Cost Tracker for AI Coding Agents

AI coding agents are amazing — until the bill arrives. And it will arrive from three places at once: Claude Code in one directory, Cursor in another, Codex in a third. Each logs usage in its own JSONL format, with different field names and different pricing models.

So I built a small local tool that reads all three, normalizes them, and produces one cost report — by model, by day, by agent — with budget guardrails that warn at 80% and flag at 100%. Here's what it took.

The Hard Part: Three Different Log Formats

Each agent logs usage differently:

// Claude Code  usage nested under message, with cache fields
{"type":"assistant","message":{"model":"claude-sonnet-4-20250514",
  "usage":{"input_tokens":523,"output_tokens":187,"cache_read_input_tokens":1200}},
 "timestamp":"2026-08-01T10:00:00.000Z"}

// Codex CLI  usage nested under payload
{"type":"response_item","payload":{"type":"message","model":"gpt-5-codex",
  "usage":{"input_tokens":2000,"output_tokens":800}}}

// Cursor  different field names entirely
{"type":"assistant","message":{"model":"gpt-4o",
  "usage":{"prompt_tokens":800,"completion_tokens":300}}}
Enter fullscreen mode Exit fullscreen mode

The parser can't assume a shape. Instead of three hand-written parsers, I wrote one recursive extractor that walks the object tree, finds any usage object, and pulls the tokens with fallbacks:

function extractUsage(obj, out, parentTs) {
  if (!obj || typeof obj !== 'object') return;
  if (obj.usage) {
    out.push({
      model: obj.model || 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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That handles all three formats (and future ones) with one code path.

The Pricing Engine: 23 Providers, Cache Discounts

The real insight: you don't need per-model accuracy to be useful — you need good enough to spot runaway spend. I reuse a pricing table of 23 providers (Anthropic, OpenAI, DeepSeek, Google, etc.), match by exact name or prefix, and fall back to a provider-name guess for unknown models:

function estimateCost({ model, inputTokens, outputTokens, cachedReadTokens = 0 }) {
  const price = lookupModel(model);          // $/1K tokens
  const cached = Number(cachedReadTokens) || 0;
  // cache reads are ~10x cheaper than fresh input
  const cost = (inputTokens * price.input
              + cached * price.input * 0.1
              + outputTokens * price.output) / 1000;
  return { cost, estimated: !!price.estimated };
}
Enter fullscreen mode Exit fullscreen mode

Unknown models get an estimated rate (flagged in the report), so nothing silently disappears from the total.

Budget Guardrails

The killer feature isn't the report — it's the limit. Set a monthly budget, and the tool tells you where you stand:

  • < 50% — OK
  • ≥ 80% — WARN (you'll overspend at this rate)
  • ≥ 100% — OVERRUN
Guardrail: $0.0698 / $50 (0%) → OK
Enter fullscreen mode Exit fullscreen mode

Two Forms: CLI + MCP

Developers live in terminals, but agents live in the MCP ecosystem. So the tool ships as both:

  • CLI: agentcost scan ~/.claude/projects 50 → terminal report
  • MCP server: exposes scan_cost, get_budget, set_budget tools — so your coding agent can answer "how much have I spent this month?" itself

What I Learned

  1. Field-name drift is the real costinput_tokens vs prompt_tokens vs inputTokens. One recursive extractor beats three parsers.
  2. Cache accounting matters — Claude's cached reads are ~10x cheaper; ignoring them overstates costs badly on long sessions.
  3. Estimates beat silence — an unknown model that shows up as "$0" teaches users to ignore the tool. Flag it as estimated instead.
  4. Local-first is a feature — "nothing leaves your machine" is a privacy story users actually care about, especially for cost data.

Status

The project is in early release (v0.1, 22/22 tests passing) and available on npm: agentcost-cli. This is my first open-source tool from the SellerTools family, and I'm looking for feedback from heavy Claude Code / Cursor / Codex users: what's missing for your workflow? Budgets per project? Team aggregation? Anomaly alerts? Tell me in the comments.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev

A unified cost view gets valuable when it explains behavior, not just spend. The useful split is usually planning, context reloads, tool failures, retries, and actual implementation work.