# cost analysis - projection, anomaly, burn, diff

`cost-budget-check.mjs` answers one question, live, for one run: is THIS task
about to cross its ceiling. Two ways a budget actually empties are invisible to
it. A slow drift never trips any single run's ceiling. And one pathological
session can burn a week's worth in an hour while every individual run stays
comfortably under its cap.

`cost-analyze.mjs` answers the other four questions off one series.

| Sub-command | Question |
|---|---|
| `projection` | at this rate, what does a week / a month / a quarter cost, and when does a stated budget run out |
| `anomaly` | which day or session is out of family |
| `burn` | is the last day accelerating against the preceding week |
| `diff` | two saved snapshots, side by side |

```bash
node "$HOME/.claude/scripts/cost-analyze.mjs" projection --days 14 [--monthly-usd 200]
node "$HOME/.claude/scripts/cost-analyze.mjs" anomaly --days 30 [--by day|session] [--threshold 3.5]
node "$HOME/.claude/scripts/cost-analyze.mjs" burn [--factor 3]
node "$HOME/.claude/scripts/cost-analyze.mjs" --save [--days 30]
node "$HOME/.claude/scripts/cost-analyze.mjs" diff [a.json b.json]
```

Exit 0 means nothing to report, 10 means a finding, 2 is a usage error. Every
sub-command takes `--json`, and the human output and the JSON are rendered from
the same numbers.

## Where the numbers come from, and what that makes them worth

The per-run token accumulators in `tracker-state.json` are the pipeline's own
ledger, and they are the obvious source. They are also empty: the schema has
`tokens_in` / `tokens_out` / `tokens_cached` per phase, they are written only
when a phase reports them, and on a real machine no tracker carries them at all.

The series that does exist is the host's own transcripts, one usage record per
assistant turn, under `~/.claude/projects/<slug>/<session>.jsonl`. So that is
what this reads, and three consequences follow that are stated here rather than
discovered later:

- It covers everything Claude Code did on this machine, not only pipeline runs.
- The figures are ESTIMATES priced from `cost-table.json`, at LIST price. On a
  subscription they are the right number for comparing one day to another and
  the wrong number to call a bill. The human output says so on every run.
- A host with no such transcripts (Copilot, Codex) reports `UNMEASURED`, never
  zero. Zero would read as "you spent nothing" when it means "I could not see".

Records whose model is not in `cost-table.json` are COUNTED as unpriced and left
out of the total. Folding them in at zero would make an unknown model look free,
which is the direction that hurts.

Cache writes are priced at 1.25x input, derived rather than read from the table,
because `cost-table.json` carries a cache READ rate only. Dropping the component
would make every long-context session look cheap.

## Why the median and the MAD, not the mean

The single expensive session `anomaly` exists to find is also the observation
that inflates a mean and a standard deviation - it hides inside the statistic
measured against it. With six ordinary days and one at fifty times the rest, a
mean-based z-score puts the outlier at about 2.3 sigma, under every usual
threshold. The median does not move for one observation, and neither does the
median absolute deviation.

The score is the modified z-score of Iglewicz and Hoaglin, `0.6745 * (x - median)
/ MAD`, flagged at 3.5.

When more than half the values are identical the MAD is zero. That is not an
error and not a licence to divide by zero: the published fallback is the mean
absolute deviation scaled by 1.253314. When THAT is zero too there is no
dispersion at all, and the honest answer is that no anomaly can be called -
which is what it says.

Fewer than five points is refused outright, with the number it needs.

## Why projection divides by the calendar window

Total over the window, divided by the WINDOW, not by the days that happen to
have data. Dividing by active days answers a different question - what a working
day costs - and then projecting a month as thirty working days overstates it by
roughly a third.

`--monthly-usd` is optional and there is no default. Without it the projection
is reported and the exhaustion date is not, with the reason given. Inventing a
budget to compare against would produce a number that looks measured.

## Snapshots

`--save` writes `~/.claude/state/cost/<iso>.json`: the window, the total, the
per-day rate and the per-day breakdown. `diff` compares two, by path or the last
two by default, and says when the windows differ - the per-day rate is
comparable across different windows, the total is not.
