The Most Useful Line on Your AI Cost Report Is the One You Can’t Explain

Attribution, allocation, and why “unknown” belongs in the schema.

This piece grew out of a comment thread on Sarvar Nadaf’s Per-Agent Cost Tracking for Multi-Agent AI on AWS. The schema below was worked out in that conversation, in public, and it is better for it. Where a specific idea came from the exchange, I have tried to say so.


Most AI cost dashboards answer one question well: how much did this run cost. Total tokens, model spend, per-agent spend, latency, tool usage. Those are real and useful numbers, and for a while they are enough.

They stop being enough the moment your system becomes a composition. Once a request flows through a retriever, a knowledge graph, three specialist agents, and a supervisor that synthesizes their output, the total tells you almost nothing about what to change. A run can be correct, return HTTP 200, look healthy in every latency-and-errors panel, and still cost forty percent more than an identical run that produced the same answer. The overspend is real. It is just not anywhere you are looking.

To find it, you have to stop asking where the money was spent and start asking what caused it to be spent. Those are different questions, and the gap between them is the whole subject of this piece.

Where a Cost Is Incurred Is Not What Caused It

Consider a retrieval operation that costs $0.004: searching, ranking, fetching. That is the direct cost, and it is easy to attribute. It happened on that span, you can measure it, done.

Now suppose that retrieval returned 20,000 tokens, and all of them were hydrated into a supervisor’s context on the next step. The supervisor then costs $0.009. How much did the retrieval really cost?

The direct answer is still $0.004. But that is no longer the interesting answer, because the retrieval also caused cost somewhere else. It inflated the supervisor’s context, and some portion of that $0.009 exists only because the retrieval handed it too much material. The cost was incurred at the supervisor. It was caused, in part, at the retriever.

This gives you two distinct dimensions, and a useful cost model has to carry both:

  • Where the cost was incurred. This is just the span. Directly observed, low ambiguity.
  • What caused or contributed to it. This is the interesting axis, and it is the one no aggregate dashboard shows.

A record that captures both might look like this:

span_id: retrieval-104
direct_cost: $0.0040
primary: retrieval
returned_tokens: 20000

downstream:
  span_id: supervisor-105
  attributed_cost: $0.0021
  caused_by: retrieval-104
  attribution_method: proportional

The dollars are single-counted. We do not charge the $0.0021 twice. The supervisor genuinely incurred it; the retrieval genuinely contributed to causing it; and the record says both without inventing money. What we have added is lineage: a link from a downstream cost back to the decision that helped produce it.

The Hard Part Is Honesty About How You Know

Here is the question that breaks naive versions of this: how do we know retrieval-104 actually caused $0.0021 of the supervisor’s cost, and not some other amount?

Sometimes you can measure it. If you have a controlled comparison where the only meaningful change is that retrieval result, the delta is real evidence. Supervisor costs $0.006 without the retrieved material and $0.009 with it, so roughly $0.003 of downstream cost is attributable to that retrieval. That is measured causation, and it is the strongest claim you can make.

Most production traces do not give you that. In a real run the supervisor is carrying system instructions, conversation state, the outputs of other agents, tool results, and the retrieved material, all at once. There is no clean counterfactual. So you fall back on allocation: split the supervisor’s context cost proportionally by the tokens each source contributed. That is a reasonable method. It is not measurement, and the receipt must not pretend it is.

This is why the single most important field in the whole schema is not a dollar amount. It is this:

attribution_method:
  - measured_delta
  - proportional
  - estimated
  - unknown

That field is what keeps the entire model honest. It stops a proportional guess from masquerading as measured causation. With it, a line can say “retrieval span 104 contributed an estimated $0.0021 of downstream context cost, allocated proportionally by hydrated token share,” and every word in that sentence is defensible, because the method is stated. Without it, the same $0.0021 acquires a precision the evidence never earned.

Resist collapsing this into a confidence score. A number like confidence: 0.82 feels rigorous and gives you nothing, because now you have a second number whose provenance you have to go investigate. measured_delta, proportional, estimated, and unknown each tell you why you are entitled to believe the figure. The method is the provenance. A score would hide it.

Show the Method Where the Decision Is Made

A natural instinct is to keep the attribution method as drill-down metadata, out of the main view, so the report stays clean. That instinct is wrong, and it is wrong for the same reason aggregate dashboards are wrong: it makes two different claims look equivalent.

The method belongs inline, next to any attributed cost, with one sensible exception. A directly observed cost carries no ambiguity and needs no method tag:

retrieval-104   RETRIEVAL   $0.0040

There is nothing to disclose there; it was measured on the span. But the moment a number is attributed rather than observed, the method has to ride along:

retrieval-104 -> downstream CONTEXT   $0.0021   proportional

Drop the word proportional and that $0.0021 visually becomes as solid as the $0.0040 above it, which is a lie of formatting. A report that hides the distinction between what it measured and what it allocated has committed the same sin as the dashboard that only shows a total. If two numbers make materially different claims, the interface must not make them look the same.

So the main report shows amount, category, direct versus downstream, and method. The drill-down holds the evidence behind the method: hydrated token counts, comparison runs, parent-child span references, the assumptions the allocation rests on. The decision surface stays readable; the receipt is one click away, not dumped into the table.

“Unknown” Is Not a Gap in the Accounting

Every honest version of this schema has to allow unknown as a real value, not a placeholder you feel bad about. And once you sit with it, the unknown rows turn out to be the most useful rows in the report.

A high downstream cost with unknown attribution is not incomplete bookkeeping. It is the system telling you exactly where your observability boundary stops letting you explain its own behavior. It is pointing at the place where you cannot yet answer “what caused this,” which is the place most worth instrumenting next. A tidy report with no unknown rows has usually not achieved understanding. It has hidden its ignorance behind confident allocation.

There is also a real reason unknown is sometimes the only honest answer: context is not additive. An extra 5,000 tokens of context does not simply add a proportional slice of cost. It can change caching behavior, alter the reasoning path the model takes, or change how much output the model generates downstream. When that happens, token share and cost share stop mapping to each other cleanly, and any proportional number you report is a polite fiction. In those cases the schema should say unknown and mean it, rather than allocate a figure it cannot defend.

Read that way, the report stops being a statement of where you paid and becomes a map of two things at once: what you can explain about your spending, and where your ability to explain it runs out. The second map is the one that tells you what to build.

What This Actually Costs to Build

The reason this is not a research project is that most of the structure already exists. If your traces are parent-child spans, the causal lineage is physically present already; a downstream cost sits under the decision that produced it. You are not inventing a new tracing mechanism. You are making the attribution semantics explicit on top of a trace you already record.

Concretely, that is a small number of additions. Stamp a primary category on each span. Add a contributes_to link and a cause on spans that produce downstream effects. Add the attribution_method on any attributed cost. Then roll the report up along two axes, category and direct-versus-downstream, and let unknown be a first-class row rather than a swept-under one. The intelligence is not in the plumbing. It is in having the honest method field and being willing to publish the unknown rows.

One warning from the same conversation that produced all this: how you record and how you attribute are coupled. Change the way spans are emitted and you can silently break the logic that reads them. The defense is the same one that makes the whole model trustworthy, a known-good baseline you compare against, so that when your instrumentation shifts under you, the numbers move and you notice.

The Point

We have spent a lot of effort making AI spend visible. Better token counts, per-agent breakdowns, nested traces. All of it answers “how much.” Almost none of it answers “why,” and “why” is the only version of the question you can act on.

The move from one to the other is not a bigger dashboard. It is a small, honest schema: separate where a cost was incurred from what caused it, state the method behind every attributed number, and treat the costs you cannot explain as signal rather than embarrassment. Do that, and the report stops telling you what you spent and starts telling you what to fix, including, in the unknown rows, where to look first.

Even the cost report, it turns out, needs provenance. Not just the amount and the category, but how sure you are about who to blame. That last column may be the most useful one on the page.


With thanks to Sarvar Nadaf, whose post and the conversation under it produced this schema, and to the commenters in that thread who pushed on the baseline and the propagation. The receipt is better for the argument.

Facebooktwitterredditlinkedinmail

The Verification Bottleneck in AI-Generated Software

AI can generate code faster than ever. That doesn’t mean we’re shipping correct software faster.

Recently, I asked a coding agent to build a password reset flow. It produced the route, the token handling, the email integration, and the UI in about five minutes.

The implementation had a bug. The reset link worked more than once. Use it to set a new password, then open the same link again, and it still worked.

The feature request hadn’t explicitly said that a reset link should be single use. It wasn’t in the feature request, because it’s the kind of thing that goes without saying right up until the moment it doesn’t. Every path a person would click through by hand worked perfectly. A code review might have caught it. A demo would not have.

That gap is what I want to talk about.

Code generation is no longer the expensive part

Traditional software development has an obvious constraint: humans have to write the software. We think about the requirement, design the implementation, write the code, run it, discover that it doesn’t work, debug it, and repeat until we’re sufficiently convinced.

AI coding agents compress parts of that loop dramatically. The agent can often produce an implementation faster than I can thoroughly review what it generated, which creates an inversion. For a long time, writing code was expensive and checking it was comparatively cheap. What happens when writing becomes cheap?

Verification becomes proportionally more valuable.

Suppose an agent implements a feature in five minutes, but determining whether the implementation is correct requires another 45 minutes of manual testing and code review. We haven’t created a five-minute development process. We’ve created a 50-minute development process with a very fast implementation stage.

And the verification half is harder than it used to be, because you’re now auditing code you didn’t write.

Generating more code doesn’t solve that. We need a better feedback loop.

The specification is becoming the durable artifact

Here’s the shift I think matters most, and it goes well beyond testing.

If an agent can rewrite a component cheaply, we become less attached to any particular implementation. Implementations are becoming disposable. Six months from now the routes change, the framework changes, the DOM changes, the internal architecture changes.

The user requirement doesn’t:

A user who resets their password must subsequently be able to authenticate with the new password, must no longer be able to authenticate with the old one, and must not be able to reuse the reset link.

In an era of inexpensive code generation, the thing worth maintaining is increasingly the specification rather than the implementation. Which means it’s worth writing the specification first, deliberately, as an artifact in its own right rather than as documentation of something already built.

Start with behavior, not implementation

I tested this with a small web application, Claude Code, and testRigor.

The application was deliberately ordinary: authentication, user accounts, login behavior, the kind of functionality in countless business applications. I wasn’t interested in whether an agent could produce something visually impressive. I wanted to answer a different question. Can we define expected behavior first, let an agent implement it, and use independent end to end verification to determine whether the agent actually succeeded?

Consider the password reset feature. The implementation involves a route, form handling, token generation, validation, password hashing, database updates, session behavior, error handling, and UI changes. An agent generates all of it quickly.

None of those implementation details are what the user cares about. The user cares whether they can request a reset, receive the email, set a new password, log in with it, and whether the old password and the used link both stop working.

Those are observable outcomes. That makes them a useful boundary between what we asked the agent to build and what the agent actually built.

Executable specifications

This is where testRigor became interesting to me.

testRigor expresses tests as English-like behavioral instructions rather than requiring the test author to work primarily in selectors and automation code. A test describes what a user does and what they expect to see.

That creates an opportunity when combined with AI-assisted development, because the behavioral test can function as an executable specification.

Instead of telling an agent to “add password reset,” we provide a behavioral contract. The application must email a reset link. The link must lead to a form. The new password must work afterward. The link must not work a second time.

Now there are two artifacts with two responsibilities. The coding agent is responsible for figuring out how to implement the behavior. The behavioral test is responsible for determining whether the behavior exists.

That separation is the point.

Don’t let the student grade the exam

AI coding tools are increasingly capable of generating their own tests, and that’s useful. I use it. But there’s a problem when the same system interprets the requirement, creates the implementation, creates the test for that implementation, and then announces that everything passes.

The system can make the same mistaken assumption in multiple places. If the agent misunderstands the requirement, it produces code consistent with that misunderstanding and tests that validate the same misunderstanding.

Everything is green, and everything is also wrong.

This is exactly what happened with the reset link. An agent asked to write its own tests for the feature it just built would have tested the happy path, because the happy path is what it understood the requirement to be. It would have passed.

The single use check existed only because a specification written before the code asked a question the feature request never raised. That’s the whole value: not that the test is in English, but that it was written by someone thinking about the requirement rather than about the implementation.

Independent behavioral verification asks a different question:

Regardless of how you implemented this, does the software exhibit the behavior we specified?

That’s closer to the question a user actually cares about.

Closing the loop

The part of this that interests me most isn’t the testing tool. It’s the feedback loop.

Once an end to end test produces a deterministic pass or failure, that result becomes input to the coding agent. The agent implements the feature, the behavioral test runs, and if the test fails, the failure returns to the agent. The agent examines the implementation, makes another change, and verification runs again.

Flowchart showing an AI-assisted software development loop. A behavioral contract is passed to an AI coding agent, which produces an implementation. Behavioral end-to-end verification checks whether expected behavior is present. Passing behavior becomes verified behavior, while failure evidence is returned to the coding agent for another implementation attempt.

In my case the full cycle ran three times. Red against the baseline, because the feature didn’t exist yet. Red again after generation, on the reused link. Green after the failure text went back to the agent as context. About twenty minutes of wall clock end to end, most of it unattended.

The failure text mattered more than I expected. It wasn’t a stack trace. It was a step in the behavior, in plain English, that didn’t happen. That’s readable by a person and readable by an agent, which is what let the loop close without me translating between them.

The agent now has something more useful than “try again.” It has evidence that a specific expected behavior was not observed.

Spec-First Verification for AI-Generated Code

The video is rough around the edges, which is probably appropriate for an experiment. The interesting part isn’t production quality. It’s watching the loop operate.

Generation and authority are different jobs

This reinforced something I’ve been thinking about more broadly in AI system design. Probabilistic systems are extraordinarily useful for proposing things: generate this implementation, interpret this requirement, suggest a fix, explain this failure, determine which files probably need to change.

But there are places where I want something else to have authority. Did the build succeed? Did the API return the expected response? Does the database contain the expected state? Can the user complete the specified workflow? Did the test pass?

Those questions can be answered deterministically, which gives a useful architectural separation:

AI proposes. Deterministic systems verify.

It doesn’t eliminate mistakes. A badly written test verifies the wrong thing. An incomplete specification leaves important behavior uncovered. A test environment can differ from production. Verification itself needs engineering.

But separating generation from verification means we stop treating the model’s confidence as evidence that the implementation is correct.

Behavioral tests aren’t magic

Working with an English-like testing system reinforced another lesson: plain English does not mean no learning required.

Tools still have semantics. You need to understand how the system identifies elements, interprets instructions, manages state, handles authentication, and responds when the application doesn’t behave as expected. There were several moments where I had to learn testRigor’s particular vocabulary before a seemingly obvious instruction did what I expected. My spec assumed the application would land on a login form after a successful reset. It didn’t. That’s a gap in my specification, not a bug in the application, and finding it took a run I hadn’t budgeted for.

That isn’t a flaw unique to testRigor. Abstractions don’t eliminate complexity. They move it.

SQL didn’t eliminate the need to understand databases. High level languages didn’t eliminate the need to understand software. Natural language testing doesn’t eliminate the need to understand testing.

What changes is who can express the behavior, and how tightly that behavior is coupled to implementation details. That part is genuinely interesting.

What we should be optimizing

Much of the excitement around AI-assisted development has focused on productivity. How much faster can developers write code? How many tasks can an agent complete? How many tokens did it use?

Those metrics aren’t useless, but they aren’t the outcome. Software exists to behave correctly enough to solve a problem.

If an agent generates 10,000 lines in ten minutes and we spend the rest of the day figuring out whether any of it works, those lines aren’t evidence of productivity. They’re inventory awaiting inspection.

The metric worth tracking is correct functionality delivered per unit of time. That includes generation, but it also includes verification. As generation approaches zero marginal effort, the second half is where the remaining cost lives.

Coding agents are going to get better. They’ll generate larger changes, operate longer without supervision, understand more complex repositories, and handle more of the implementation process autonomously. That makes verification more important, not less. Greater autonomy without independent evaluation doesn’t produce a better development system. It produces a faster source of unverified output.

The opportunity is to build verification into the architecture rather than bolting it on afterward. The specification defines the destination. The coding agent proposes a route. Verification tells us whether we actually arrived.

Not generating code faster. Shipping working software faster.

Facebooktwitterredditlinkedinmail