Academyby Dasow

Stop 9 of 10 · Weekly

Cost and usage ledger

One wrapper that records tokens per tool and per client, a dated rates file the studio types by hand, a weekly report with the denominator that matters, and the question that kills a tool.

Watch first · 0:57

Six tools, one bill, no idea which

By stop 8 the studio has a checker, a diagnoser, a ticket writer, an eval harness and two helpers Mo built in an afternoon. They all call the same API with the same key. The invoice is one number. Nobody can say whether the diagnosis step is worth what it costs, which means nobody can delete anything.

One wrapper fixes that, and it is the only way any tool is allowed to call the API.

export async function call({ tool, clientId, runId, model, messages }) {
  const res = await anthropic.messages.create({ model, messages, max_tokens: 1024 });
  const u = res.usage;
  db.prepare(`insert into usage_log
    (at, run_id, tool, client_id, model, input_tokens, output_tokens, cache_read, cache_write)
    values (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
      new Date().toISOString(), runId, tool, clientId, model,
      u.input_tokens, u.output_tokens, u.cache_read_input_tokens ?? 0, u.cache_creation_input_tokens ?? 0
    );
  return res;
}

tool and clientId are required arguments, so a call with no owner does not compile in review. runId ties every call in one night together, which is how you find the night a retry loop ran four hundred times. The two cache columns are there so you can see whether caching is doing anything, rather than believing it is.

Rates live in a dated file

rates.json holds one entry per model, typed by hand from claude.com/pricing, with the date it was read and the initials of whoever read it. The report multiplies tokens by rates at report time. A model with no entry fails the report by name.

The weekly report

Line What it answers
Spend by tool Which of the six is actually the bill
Spend by client What a retainer costs to serve, for the pricing conversation
Cost per acted-on output The only number that says whether a tool earns its place
Cache read share Whether the prompt caching Mo added is doing what he thought

The third line needs a definition the studio agrees once and then stops arguing about. Dany's: a finding whose ticket someone opened, commented on or closed within seven days.

Build the report
Write scripts/usage-report.mjs. It reads usage_log for a date range and rates.json, and prints four sections: spend by tool, spend by client, cost per acted-on output by tool, and cache read share by tool.

An acted-on output is a finding whose ticket has any human activity within seven days of filing. Join through the findings table.

Fail with a named error if any model in the range has no entry in rates.json, and print the date on the rates file at the top of every report so nobody quotes a number without knowing which rates made it.
The kill decision
Here is my weekly tool report and the same report from four weeks ago: [paste both reports].

For each tool, answer three questions from the numbers alone. Did a person act on anything it produced this month. What does one acted-on output cost compared with the same tool four weeks ago. And would deleting it move any number a client can see.

Then rank the tools by the case for keeping them, and name the one you would delete first with the sentence I should put in the commit message.

Dany deleted the report-summarising helper Mo built in an afternoon: forty runs, three outputs anyone opened, and the cost per acted-on output was the highest of the six. The checker, which produces the most rows, was the cheapest by that measure.

Quick check

Try it

Report a bug or share feedback