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.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add mediar-ai/skillhubz --skill claude-usage-calc --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Claude Usage Calc?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mediar-ai-claude-usage-calc)More formats (shields.io, HTML) on the badges page.
---
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)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!