Tracking Down Agent Spending Anomalies With One Usage Ledger
Every model call our agents make is written to one ledger row with who made it and why. Here are four anomalies from the last month, what each looked like at first, and the query that explained it.
All of our model traffic goes through one gateway: coding agents, chat in the UI, background summarizers, app features. For every call, the gateway writes one row to a usage_event table in Postgres. Over the last 30 days that table gained about 1.16 million rows, 114 billion tokens, and roughly $3,080 of estimated spend.
A provider dashboard can tell you that spend went up. It cannot tell you which agent, task or feature caused it, because it only sees an API key. The ledger exists to answer that second question. Each row carries the tokens, plus the keys we found we needed when something looked wrong:
- Who: user, API key, tenant and the agent role (a coding agent, the chat UI, a summarizer).
- What work: session, conversation, ticket, workstream and pull request.
- Where: model, backend, machine, pod and source address.
- Tokens, split the way the provider reported them: uncached input, cache reads, cache writes, output and reasoning.
A command-line tool ranks spend by any of these keys over any window (top --by role, --by day, --by session and so on). It reads Postgres directly, so there is no separate analytics pipeline that can fall behind. The four cases below are all from the last month.
1. Spend more than doubled in two days
On 3 and 4 October, estimated spend jumped to $221 and $245 a day, against a 28-day average of about $100. The first instinct was a runaway agent stuck in a loop. Ranking those two days by session showed no single outlier: the top sessions each used about 3% to 7% of the total, and their average prompt size matched every other day.
Show the data
| USD | |
|---|---|
| Sep 25 | 40 |
| Sep 26 | 59 |
| Sep 27 | 63 |
| Sep 28 | 71 |
| Sep 29 | 63 |
| Sep 30 | 73 |
| Oct 1 | 83 |
| Oct 2 | 106 |
| Oct 3 | 221 |
| Oct 4 | 245 |
| Oct 5 | 95 |
| Oct 6 | 101 |
| Oct 7 | 159 |
| Oct 8 | 106 |
| Oct 9 | 141 |
| Oct 10 | 74 |
| Oct 11 | 177 |
| Oct 12 | 96 |
| Oct 13 | 146 |
| Oct 14 | 88 |
| Oct 15 | 121 |
| Oct 16 | 81 |
| Oct 17 | 57 |
| Oct 18 | 127 |
| Oct 19 | 98 |
| Oct 20 | 94 |
| Oct 21 | 87 |
| Oct 22 | 93 |
Counting distinct agent sessions per day explained it. The fleet grew from 27 coding-agent sessions on 2 October to 45 and then 51, and each session made somewhat more calls. Spend per active session moved much less than total spend did, and those were also the two busiest merge days of the month, with 197 and 182 pull requests merged. The fleet was doing more work, not repeating itself.
Show the data
| sessions | |
|---|---|
| Sep 25 | 19 |
| Sep 26 | 23 |
| Sep 27 | 19 |
| Sep 28 | 20 |
| Sep 29 | 19 |
| Sep 30 | 22 |
| Oct 1 | 28 |
| Oct 2 | 27 |
| Oct 3 | 45 |
| Oct 4 | 51 |
| Oct 5 | 47 |
| Oct 6 | 43 |
| Oct 7 | 48 |
| Oct 8 | 49 |
| Oct 9 | 38 |
| Oct 10 | 29 |
| Oct 11 | 37 |
| Oct 12 | 38 |
| Oct 13 | 36 |
| Oct 14 | 36 |
| Oct 15 | 19 |
| Oct 16 | 15 |
| Oct 17 | 15 |
| Oct 18 | 13 |
| Oct 19 | 17 |
| Oct 20 | 29 |
| Oct 21 | 24 |
| Oct 22 | 20 |
Lesson: before looking for a bug, divide by the number of things running. A spike that disappears per session is a capacity decision, not an incident. It may still be the wrong decision, but it needs a different conversation.
2. Prompts appeared to shrink by 75%
In mid-October, average input per request fell from about 125,000 tokens to as low as 20,000 on some days. That looked like good news: perhaps context trimming had started working. It was wrong.
The two providers report input differently. OpenAI counts cached tokens inside its input total and reports the cached part as a subset. Anthropic reports uncached input, cache reads and cache writes as three separate numbers. A Claude call with a 140,000-token conversation typically records a few hundred tokens of input and the rest as cache reads. When agents shifted part of their work to Claude in mid-October, the naive average fell, even though prompts had not changed.
Show the data
| Week of | Total (k tokens) | Raw (k tokens) |
|---|---|---|
| Sep 25 | 128 | 115 |
| Sep 26 | 134 | 124 |
| Sep 27 | 124 | 124 |
| Sep 28 | 127 | 126 |
| Sep 29 | 116 | 116 |
| Sep 30 | 133 | 89 |
| Oct 1 | 132 | 123 |
| Oct 2 | 123 | 123 |
| Oct 3 | 125 | 125 |
| Oct 4 | 129 | 129 |
| Oct 5 | 130 | 130 |
| Oct 6 | 136 | 136 |
| Oct 7 | 135 | 134 |
| Oct 8 | 120 | 110 |
| Oct 9 | 130 | 125 |
| Oct 10 | 141 | 90 |
| Oct 11 | 161 | 55 |
| Oct 12 | 144 | 74 |
| Oct 13 | 154 | 72 |
| Oct 14 | 134 | 96 |
| Oct 15 | 149 | 101 |
| Oct 16 | 160 | 117 |
| Oct 17 | 136 | 20 |
| Oct 18 | 181 | 30 |
| Oct 19 | 142 | 48 |
| Oct 20 | 133 | 124 |
| Oct 21 | 136 | 121 |
| Oct 22 | 145 | 63 |
With input counted once, the average rose slightly over the month, from about 125,000 to about 140,000 tokens. Our reporting now stores a normalized total next to the raw provider fields, and every cache hit rate is computed against that total.
Lesson: never average a token field across providers without checking what it includes. Keep the raw fields, because they map to how each provider bills, and add one normalized total for comparisons.
3. Calls nobody owned
About 4.4% of calls and $157 of the month’s spend had no agent role. The ledger has a view for exactly this slice, broken down by model and session. Broken down by model, half of it was calls to our main agent model that arrived without a role header, and another 30% was two smaller models that are not used through the normal agent launcher. The breakdown by session points to the specific clients to fix.
Unowned spend matters more than its share suggests. A budget that is enforced per role cannot stop a client that has no role. A growing unattributed slice is best treated as a bug in whichever client stopped sending headers.
Lesson: attribution has to be stamped by the client making the call, or by the gateway from the client’s credential. Anything that relies on someone joining logs later will drift.
4. The busiest session was not the expensive one
Ranking session-hours by call count, the top entry was one agent making 894 calls in a single hour, about one every four seconds. It looked like a loop. Its uncached input for that hour was close to zero, and its output was small: it was reading and editing many small files, with nearly all of its context served from cache.
The sessions that did cost money looked ordinary by call count. Their cost came from cache misses on large contexts: a session that switches model, or has its history rewritten, pays full price for 100,000 or more tokens on the next call. One older model on our gateway ran at a 27% cache hit rate for the month, against 91% to 97% for the models agents use day to day on the few clients still using it, so most of its input was billed at full price.
Lesson: rank by estimated cost, not by request count. Then check the cache hit rate of the most expensive sessions before anything else.
The order we check things in now
- Spend per day, then per active session. If the per-session line is flat, the cause is concurrency.
- Normalized input per call and cache hit rate, by model. A falling hit rate on the same work means something changed the prompt prefix.
- The unattributed slice. If it grew, find the client that stopped sending headers.
- Top sessions by estimated cost for the window, and what ticket or pull request each was working on.
None of this needs special infrastructure: one wide table, written once per call by the component that already sees every request. The hard part was deciding up front which keys to stamp, and adding the ones we missed each time an anomaly could not be explained.