vegman.dev
All posts

45% of My Model Spend Went to One Question the Agent Asked Itself

2026-07-31case

I opened my bill for language models and saw a number bigger than usual. Over that same period not a single new user had shown up, I hadn't shipped any new features, and load hadn't grown. The system was sitting there servicing itself - and that cost money.

This is about my own agent system, not a client project. An agent here is a program that decides on its own what to do next, on a schedule or in response to an event, and calls a language model at every step. Such a program has an unpleasant property: it asks questions even when there's nothing to answer.

When I finished the analysis, it turned out that 45% of everything I had ever spent on models went to one single question the agent asked itself: "is there any work for me?" Not on the work - on the question about the work.

By the end of this article you'll understand why the main cost line in agent systems is idling rather than useful action, and how to build accounting that answers "where did it go" instead of only "how much is left."

The symptom: the bill grew, the user count didn't

The usual reaction to a bigger bill is to look for the cause in load. Somebody wrote a lot to the agent, somebody uploaded a big document, a retry loop kicked in somewhere. But there was no load: same number of users, same number of tasks, same functionality.

I noticed the second symptom almost by accident. The context the agent was sending to the model was growing on its own. Over two and a half hours of observation, the input size went from 29,643 to 29,707 tokens. A token is a chunk of text - model input and output are counted in tokens, and that's what the provider bills you for.

Sixty-four tokens over two and a half hours is trivial in itself. What matters isn't the size, it's the sign. Spend was growing without a single new user. A system that gets more expensive while idle will keep getting more expensive: the growth mechanism is built in, and it doesn't depend on whether anyone is using it.

Eleven days blind: why the cause stayed hidden

Eleven days passed between "the bill grew" and "here's the cause." The whole time, the spend was perfectly visible in dollars and completely invisible in causes. I knew the amount and didn't know what it was for.

The reason is annoying. My monitoring answered the question "how much is left": remaining limit, spent this period, are we approaching the threshold. That kind of monitoring honestly warns you that the money is running out and says nothing at all about where exactly it's going. What I needed was a different question - "where did it go": a breakdown by project, model, agent, and day.

The other half of the problem was that accounting lived in two layers that can't see each other. My analysis ran over one layer while the money was burning in the other. Until those layers are pulled into a single table, any analysis turns into guesswork: you confidently explain the part of the spend that happened to land in your hands, and you don't suspect the rest exists. For eleven days I was explaining the smaller half of the bill to myself.

flowchart LR
  A["Счет провайдера"] --> D["Одна таблица:<br/>проект x модель x агент x день"]
  B["Логи агентов"] --> D
  C["Выгрузка по проектам"] --> D
  D --> Q["Вопрос «какой агент<br/>съел эти деньги»"]
  A -.->|"поодиночке"| X["Объяснима только<br/>часть счета"]
  B -.-> X
  C -.-> X
Пока слои учета лежат врозь, каждый отвечает на свой вопрос - и ни один не отвечает на «куда ушло»

What the analysis showed: the price of "do I have anything to do?"

When I finally pulled everything into one table, the largest cost line turned out to be not work but polling. 1007 calls, $13.83-45% of all model spend over the entire life of the system.

The mechanics are simple. The poll ran every 30 minutes, around the clock, against an empty task list. Forty-eight times a day the system woke the agent and asked whether there was any work for it. There never was.

The most interesting part is the ratio inside each call. Every such request was roughly 29,700 input tokens against a response of about 90 tokens. That is, every half hour the agent's full context went off to the model: system instructions, tool descriptions, accumulated state - all for the phrase "I have nothing to do." The input is hundreds of times bigger than the answer.

Which leads to a conclusion worth taking away separately from my story. In agent systems you pay mostly for input, not output. The answer is short, while the context the agent needs to make a decision is long, and it gets sent in full on every call - including the ones where the decision is known in advance and equals "do nothing."

The intermediate-optimization trap

Before I found the cause, I did exactly what everyone does: I moved the call to a cheaper model. The savings came to 7x on that same call. A pretty number; it would have made a great report.

Except I made cheaper what I should have turned off. A sevenfold savings on a useless action is still spending, just tidier. Worse, that kind of optimization closes the question psychologically: the chart went down, so you did your job, so you can move on to something else. The spend stayed, but it stopped being irritating - and therefore stopped being investigated.

The order of operations should be the reverse. First "is this call needed at all," then "which model makes it." Optimizing the model without checking whether the call is necessary is penny-pinching.

Documentation describes intent, not behavior

Next came an attempt to turn the polling off the honest way. The platform has a service file listing periodic tasks, and the documentation flatly promised: keep the file empty and there will be no calls.

I reduced the file to nothing but comments. The poll ran again, right on schedule.

The factual conclusion: polling is enabled at the platform level and isn't suppressed by the contents of that file. The methodological conclusion matters more. The documentation described the authors' intent, not the system's behavior. That's not deception and usually not even a mistake: it was written that way when that was the plan, then the behavior drifted away from the text, and the text stayed.

The practical implication for you: turning something off counts as turned off only after you've looked at the logs or the bill and confirmed the calls really aren't happening. Checking against the documentation is checking someone else's intentions - and what you pay for is behavior.

flowchart LR
  subgraph BEFORE["До разбора"]
    B1["Опрос каждые 30 минут,<br/>круглосуточно"] --> B2["1007 вызовов"] --> B3["13,83 $ - 45%<br/>всех расходов"]
  end
  subgraph AFTER["После выключения"]
    A1["Опроса нет"] --> A2["0 вызовов"] --> A3["0 $"]
  end
  BEFORE ==>|"проверено по счету,<br/>а не по документации"| AFTER
45% всех расходов на модели - это опрос «нет ли для меня работы» при пустом списке задач

How to count honestly: the analysis tool

To avoid repeating eleven days of blindness, I wrote a script that pulls every accounting layer into one table: project by model by agent by day. One flat table you can query with "which agent, in which model, on which day ate this money."

The characteristics that are essential for a tool like this:

  • Zero model calls. A spend-analysis tool shouldn't spend anything itself. This is pure log parsing, with no AI inside.
  • About 3 seconds for 125 megabytes of logs. If the analysis takes a long time, you'll run it once a month, when what you need is to run it the moment the question comes up.
  • 4% divergence from the provider's billing. Billing is the bill the model provider issues. If your analysis doesn't reconcile with the bill, you're explaining the wrong spend.

And three principles from observability practice - that is, from the practice of structuring logs so they can answer a question you didn't ask in advance:

  • The provider's number beats a tokenizer recount. A tokenizer is a library that splits text into tokens the same way the model does. It gives you a similar number, but that's not what you're billed on.
  • Recount with a tokenizer only where the provider didn't return a number. That's a patch over a hole in the data, not a source of truth.
  • Where there's no price, write "no price," not zero. Zero silently adds into the total and makes the report wrong but plausible. "No price" is visible to the eye, and it forces you to go figure it out.

Put it to work

What to check in practice

  • Find the calls in your logs that happen strictly on a schedule rather than in response to a human action or an event. Sort them by count, not by cost - the most frequent line is usually also the most expensive one.
  • For each periodic call, look at the ratio of input tokens to output tokens. A ratio in the hundreds is a sign that you're hauling the full context around for a one-word answer.
  • Calculate what share of your bill consists of calls after which the system did nothing. That's the price of idling.
  • Check how many accounting layers you have and whether they can see each other. If your analysis runs on one source and the bill comes from another, pull them into a single table of the form project - model - agent - day.
  • Turn off one periodic call using the method the documentation promises, and wait for the next scheduled firing. Look at the logs, not at the config.
  • Before switching to a cheaper model, answer in writing: what breaks if this call isn't made at all. If the answer is "nothing," there's nothing to make cheaper - it needs to be deleted.

How to know it worked

The criterion isn't "the bill got smaller." The bill can shrink just because you moved garbage onto a cheap model.

It worked if three things are true.

First: within a minute, without opening the provider's console, you can name the three most expensive lines of your spend for the past week - down to the agent and the day. Not the total, the composition.

Second: your analysis reconciles with the provider's bill within a margin you understand. Mine is 4%, and I know what it's made of. If the divergence is unknown, you're still looking at part of the picture.

Third: there are no lines left in your bill with not a single action behind them. Every periodic call either leads to work, or is turned off and verified against the logs as actually turned off.

Until those three things are true, "how much is left" will keep blocking the view of "where did it go" - and idling won't give itself away by anything except a slowly growing number.