Skip to main content

claude-usage-calc

Calculate Claude Code token consumption, message counts, and list-price cost from local JSONL transcript logs. Use when the user asks "how many tokens / prompts / messages did I use", "what's my Claude cost", "usage in last N days", or anything about local Claude Code usage analytics.

Jump to install

Source facts

Repository
mediar-ai/skillhubz
Last source activity
April 11, 2026 at 21:00
Detected SKILL.md language
English
Stars
7
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
claude-usage-calc
description
Calculate Claude Code token consumption, message counts, and list-price cost from local JSONL transcript logs. Use when the user asks "how many tokens / prompts / messages did I use", "what's my Claude cost", "usage in last N days", or anything about local Claude Code usage analytics.
allowed-tools
Bash, Read
# Claude Code Usage Calculation Compute token usage, prompt counts, and cost from local Claude Code transcript files. ## Where the data lives - Transcripts: `~/.claude/projects/**/*.jsonl` (one file per session, one JSON object per line) - Each line has `timestamp` (ISO8601 UTC), `type` (`user` | `assistant` | `system`), and for assistant lines a `message.usage` block from the API response - No transcripts older than ~90d typically exist locally (rotated/cleaned) ## CRITICAL: things to get right ### 1. Each `usage` block is per-API-call, NOT per-conversation Verified by inspection: turn 1 has `cache_read=11k`, turn 146 has `cache_read=89k`, growing as the cached prefix grows. Summing across turns is correct for billing because each API call is independently charged. Cache reads dominating (often 90%+ of tokens) is expected and normal, not a counting bug. ### 2. DEDUPE BY `message.id` — THIS IS THE BIGGEST FOOTGUN Claude Code logs the same assistant message multiple times across parent chains and sidechain entries. **Without dedup, totals are inflated ~2.4×.** Always keep a `seen` set of `message.id` and skip duplicates. A real measurement showed 794k raw assistant entries collapsing to 376k unique turns. ### 3. "Real human prompts" ≠ all `type:user` entries The harness logs tool_result continuations as `type:user`. To count actual prompts the human sent, filter `type:user` entries AND exclude any whose `message.content` is a list containing a block with `type=="tool_result"`. In a real run: 564,909 raw user entries → 53,406 actual human prompts. ### 4. Filter by `timestamp`, not file mtime File mtime is unreliable (a session touched today may include messages from weeks ago). Parse `timestamp` per line and bucket by `(now - t).days`. ### 5. List price ≠ what the user actually pays ALWAYS state this explicitly in the report. If the user is on Max / Pro / committed-use / enterprise, real billing is dramatically lower. Tell them the authoritative number is at `console.anthropic.com` or `claude.ai` billing. ### 6. Pricing per million tokens (USD, list price) | Family | input | output | cache_create | cache_read | |---------|-------|--------|--------------|------------| | opus | 15.00 | 75.00 | 18.75 | 1.50 | | sonnet | 3.00 | 15.00 | 3.75 | 0.30 | | haiku | 0.80 | 4.00 | 1.00 | 0.08 | Detect family from `message.model` substring (`opus` / `sonnet` / `haiku`). Default unknown to sonnet. ## What NOT to do - ❌ Do not sum raw assistant entries without deduping by `message.id` - ❌ Do not count all `type:user` entries as "prompts" (most are tool_results) - ❌ Do not filter by file mtime instead of per-line `timestamp` - ❌ Do not present list-price cost as "what you paid" — always caveat it - ❌ Do not exclude cache_read from totals "because it looks too big" — it IS billed (at 0.1× input) - ❌ Do not assume `usage` is cumulative across the conversation — it is per-API-call - ❌ Do not write throwaway scripts in the repo or home dir — use `scripts/` (gitignored) if needed ## What TO do - ✅ Stream all JSONLs through one Python pass via `find ... -print0 | xargs -0 cat | python3 -c '...'` - ✅ Dedupe by `message.id` with a `seen` set - ✅ Bucket by age: `0-30d`, `30-60d`, `60-90d` for trend questions - ✅ Break out by model family (opus / sonnet / haiku) — Opus typically dominates cost - ✅ Report all four token buckets separately: input, output, cache_create, cache_read - ✅ Always show: real human prompts, unique assistant turns, total tokens, list-price cost - ✅ Caveat that list price ≠ real bill - ✅ Point user to console.anthropic.com or claude.ai billing for ground truth ## Reference script Run this to compute last-30d usage. Adjust the day window as needed. ```bash find ~/.claude/projects -name "*.jsonl" -print0 2>/dev/null | xargs -0 cat 2>/dev/null | python3 -c " import sys, json from datetime import datetime, timezone now = datetime.now(timezone.utc) WINDOW_DAYS = 30 PRICES = { 'opus': {'in':15.00,'out':75.00,'cc':18.75,'cr':1.50}, 'sonnet': {'in': 3.00,'out':15.00,'cc': 3.75,'cr':0.30}, 'haiku': {'in': 0.80,'out': 4.00,'cc': 1.00,'cr':0.08}, } def fam(m): if not m: return 'sonnet' m = m.lower() if 'opus' in m: return 'opus' if 'haiku' in m: return 'haiku' return 'sonnet' seen = set() agg = {} real_user_msgs = 0 dup_skipped = 0 for line in sys.stdin: try: d = json.loads(line) ts = d.get('timestamp') if not ts: continue t = datetime.fromisoformat(ts.replace('Z','+00:00')) if (now - t).days >= WINDOW_DAYS: continue if d.get('type') == 'user': msg = d.get('message',{}) c = msg.get('content') if isinstance(msg,dict) else None tool_result = isinstance(c,list) and any(isinstance(x,dict) and x.get('type')=='tool_result' for x in c) if not tool_result: real_user_msgs += 1 continue if d.get('type') != 'assistant': continue msg = d.get('message') if isinstance(d.get('message'),dict) else None if not msg: continue mid = msg.get('id') if mid: if mid in seen: dup_skipped += 1 continue seen.add(mid) u = msg.get('usage') or {} f = fam(msg.get('model')) a = agg.setdefault(f, {'in':0,'out':0,'cc':0,'cr':0,'turns':0}) a['in'] += u.get('input_tokens',0) or 0 a['out'] += u.get('output_tokens',0) or 0 a['cc'] += u.get('cache_creation_input_tokens',0) or 0 a['cr'] += u.get('cache_read_input_tokens',0) or 0 a['turns'] += 1 except: pass grand_cost = 0.0 grand_tok = 0 print(f'{\"model\":<8} {\"turns\":>9} {\"in\":>14} {\"out\":>14} {\"cc\":>16} {\"cr\":>16} {\"cost\":>11}') for f,a in sorted(agg.items()): p = PRICES[f] cost = (a['in']*p['in']+a['out']*p['out']+a['cc']*p['cc']+a['cr']*p['cr'])/1_000_000 grand_cost += cost grand_tok += a['in']+a['out']+a['cc']+a['cr'] print(f\"{f:<8} {a['turns']:>9,} {a['in']:>14,} {a['out']:>14,} {a['cc']:>16,} {a['cr']:>16,} \${cost:>10,.2f}\") print() print(f'real human prompts: {real_user_msgs:,}') print(f'unique assistant turns: {sum(a[\"turns\"] for a in agg.values()):,} (deduped {dup_skipped:,} duplicates)') print(f'TOTAL tokens: {grand_tok:,}') print(f'TOTAL cost (LIST price): \${grand_cost:,.2f}') print() print('NOTE: list price; real bill is lower if on Max/Pro/committed-use.') print('Authoritative usage: console.anthropic.com or claude.ai billing.') " ``` ## Reporting format Always report (in this order): 1. **Real human prompts** sent in window 2. **Unique assistant API calls** (with dedup count noted) 3. Per-model breakdown table (turns / input / output / cache_create / cache_read / cost) 4. **TOTAL tokens** and **TOTAL list-price cost** 5. **Caveat**: list price ≠ real bill, point to console for ground truth 6. If asked about trends, also bucket `30-60d` and `60-90d` and explain growth drivers (cache_read inflation from longer sessions / parallel agents is normal)
View on GitHub