EngineerPrep / Engineering Research · Cost Analysis
How 54% of our AI bill hid under a $0.00 row
2026-09-05 · 7 min read
Our AI bill was $193.50. The dashboard ranked every capability by cost, most expensive first, and put the biggest spender at the bottom with a zero next to it.
COST BY CAPABILITY · 16 ACTIVE DAYS
Synthesize Speech (expressive) 1,085 runs $105.24
Author Lesson 92 runs $11.94
Synthesize Episode Audio 80 runs $0.00
That last row is the most expensive thing in the system. It is responsible for $105.24 — 54% of the entire bill — and the accounting is not wrong.
Every system that calls a model eventually gets a bill it does not recognise. Ours arrived at $193.50 across 2,380 recorded executions — not a large number, but large enough that we wanted to know where it went before it became one. We had instrumented every capability with per-execution cost tracking precisely so that this question would have an answer.
It had an answer. The answer was wrong in a way that took an afternoon to see, and the mistake is structural rather than clever — which is why it is worth writing down. A flat cost dashboard can make exactly the same mistake, whatever is generating the rows.
The first cut was by day, most expensive first, because "which day cost more" is the question you actually have. Two days out of sixteen carried nearly three quarters of the spend:
| Day | Cost | Share | Runs | Biggest driver |
|---|---|---|---|---|
| 2026-07-26 | $88.14 | 45.6% | 1,119 | Synthesize Speech (expressive) |
| 2026-07-25 | $48.85 | 25.2% | 632 | Synthesize Speech (expressive) |
| …14 more days | $56.51 | 29.2% | 629 | mixed |
That part was honest and useful. Two days of bulk audio generation, and the rest of the window is background noise. So we went one level down, to capabilities, expecting the same clarity.
Here is the by-capability table as the dashboard first rendered it. Read it the way anyone reads a ranked list — top is worst, bottom is fine:
| Capability | Cost | Runs | Per run | Avg duration |
|---|---|---|---|---|
| Synthesize Speech (expressive) | $105.24 | 1,085 | $0.0970 | 4.8s |
| Author Lesson | $11.94 | 92 | $0.1298 | 270.6s |
| Synthesize Episode Audio | $0.00 | 80 | $0.0000 | 69.1s |
A capability that ran eighty times, took sixty-nine seconds each time, and cost nothing. Free work. The natural reading is that it is some cheap coordination step not worth thinking about, and the natural next move is to go optimise the speech synthesis at the top.
Both readings are wrong. Synthesize Episode Audio is the thing that spent the $105.24. It calls no model itself — it decides how to break an episode into utterances and then calls Synthesize Speech once per utterance, about fourteen times per episode. Every dollar it is responsible for was billed to its children.
It is two ordinary Spring beans, each with an annotation on the method:
@Service
class EpisodeAudioService {
private final SpeechService speech; // a different bean
@Capability(name = "Synthesize Episode Audio")
public Episode synthesize(Script script) {
for (Utterance u : script.utterances()) {
speech.speak(u); // ~14 calls, each one priced
}
return assemble();
}
}
@Service
class SpeechService {
@Capability(name = "Synthesize Speech")
public Audio speak(Utterance u) {
// the actual model call
}
}
synthesize() never calls a model. Its own cost is genuinely $0.00.
How we found that is the part worth repeating. The table above already held both halves — 1,085 speech calls carrying $105.24, and 80 orchestrations carrying nothing — and no amount of staring at it produces the connection, because the connection is not in it. What produced it was opening a single speech execution and seeing its parent id point back at Synthesize Episode Audio. One child, once, and the whole ranking reads differently.
That is the lesson: a parent row will fool you. Look inside a child. A parent reports what it spent, which for anything that delegates is nothing at all. Only the child knows who asked for it.
Per-capability cost accounting is exactly correct and almost useless, because in an agentic system the thing that spends and the thing that decides to spend are never the same row.
The fix is not a better query over the same flat table. It is recording the parent/child relationship between executions, and then reporting two numbers per node instead of one: what this execution spent itself, and what it and everything beneath it spent.
Here is a single episode, expanded:
EXECUTION OWN SUBTREE DURATION
▾ Synthesize Episode Audio $0.0000 $1.3155 69.1s
├─ Synthesize Speech (expressive) $0.0970 $0.0970 4.8s
├─ Synthesize Speech (expressive) $0.0970 $0.0970 5.1s
├─ …11 more utterances $1.0670 $1.0670 —
└─ Synthesize Speech (expressive) $0.0545 $0.0545 2.7s
Open the cost explorer to expand and collapse that tree yourself — collapsed, it is the view that started this investigation.
Eighty of those, 1,085 children in total, and the $105.24 that the flat table had filed under a capability nobody was looking at. The orchestrator's own cost is still $0.00 — that remains true and it is worth keeping, because it tells you its direct AI-provider cost is nil and the model calls are the entire provider bill. Orchestration is not free in any other sense: it still spends JVM time, database round trips, queue capacity and tracing volume. What the zero means is narrow, and stating it narrowly is what makes it hard to argue with. It just cannot be the only number on the row.
Once the tree existed, the operational question changed shape. "Can we make speech synthesis cheaper" is a vendor negotiation. "Do we need fourteen separate calls per episode, and are we regenerating episodes we already have" is an engineering decision, and it is only visible from the parent.
Once we started checking arithmetic rather than trusting it, two further defects turned up in the same view. Both are the sort that survive code review because the code is doing exactly what it says.
Author Lesson ran 92 times and failed 11 of them — better than one in nine. At $11.94 total, the dashboard reported $0.1298 per run. But you do not pay for outcomes, you pay for attempts, and you only get 81 lessons. The real figure is $0.1474 per lesson that exists, 14% higher.
That gap is the price of unreliability, and reporting cost per attempt hides it precisely where it matters: a capability that fails often looks cheap, and an engineer comparing two implementations picks the flakier one.
The degenerate case is worse. A capability that failed every run divided its cost by zero successes and reported $0.0000 — so the most wasteful thing in the system sorted to the very bottom as the cheapest. Cost per success is undefined there, not zero, and the view has to say "never succeeded" rather than print a number.
The daily table sorted on values already rounded to cents. Two days whose real costs differed by less than a penny ranked arbitrarily, on a page whose own heading promises "most expensive first". Sort on the raw totals, display the rounded ones, and give equal values a deterministic tie-break so the order does not change between page loads.
1. Record the parent, not just the execution. One nullable parent_execution_id is the difference between a cost table and a cost explanation. It has to be captured at call time — you cannot reconstruct the tree afterwards from timestamps, and every system we have looked at that tried has been wrong about concurrent runs.
2. Show own cost and subtree cost together. Never one without the other. Subtree cost alone double-counts as you walk down the tree; own cost alone reports orchestrators as free. Highlight the difference between them — that difference is the finding, and where they are equal you are looking at a leaf that genuinely did the spending.
3. Denominate in successful outcomes. Cost per run flatters unreliable capabilities. Cost per success is what the business is actually buying, and the gap between the two is a number worth putting on a dashboard on its own.
Every figure here is an estimate computed from per-token and per-character rates configured in our own system, not a reconciliation against a vendor invoice. The text-to-speech rate in particular is an approximation of a pricing tier, and all the audio figures scale linearly off it. We publish them because the shape is what matters — 54% of a bill hiding under a zero is the same finding at any rate — but if you are reproducing this, reconcile against a real invoice before you trust the absolute values. We are doing the same.
Want to diagnose failures like this yourself? EngineerPrep's production failure labs give you the execution tree, logs, state, tool calls and cost evidence from realistic agent incidents. Your job is to identify the failure pattern and decide whether the proposed remediation is actually safe.
Building this into a Spring Boot system? Capstead records the parent/child execution tree, own cost, subtree cost, latency and capability metadata automatically. It is open source and Apache-2.0.
The subtree view above — a whole execution tree in one call, with cost rolled up at every level — is what shipped in Capstead 0.8.0. Worth being straight about one thing: parent and child have been linked since 0.3.1, so the data was always there and we had simply never added it up.