Three posts into this series I have covered why we charge for AI at all, four ways of pricing a token that did not survive contact with reality, and the unit we settled on. All of that was arithmetic. This post is about the part that decides whether any of it reaches you: the invoice, and the project management tool that has to carry it.
The measurement was the interesting problem and the invoice was supposed to be the boring implementation. It was not. We built it on the wrong primitive, sent an inflated report to a client because of it, and migrated fifty-eight records to fix it. The failures are the useful part here, so they get the most space.
Clients are anonymised because the work is commercially sensitive. Every number is real and none are rounded.
The constraint: a unit is worthless until a tool and an invoice both accept it
By early September we had the unit. One unit is 100,000 weighted tokens, weighted tokens being input plus five times output, and the margin is two times cost, stated in the open. The derivation is in part three.
What a unit does not tell you is where the record lives. Our project management system is ActiveCollab, which bills from records attached to tasks and offers two obvious places to put a number: a time record, or an expense. That looked like a filing decision. It turned out to be the whole design.
Attempt one: recording AI usage as time. This one reached a client.
We started with time records, because a job type already existed for every kind of work and adding three more for the model tiers took ten minutes. A token record went in with a value of 0.61, meaning 0.61 units.
Then a weekly client report was previewed and the number was wrong in the worst direction. The account had a monthly budget of two hours. The report claimed nine point one used, an overrun of seven point one. Actual human hours that month: 0.81.
On the larger account the gap was wider: 43.86 hours reported against a real 15.51. Of those twenty-eight hours, 23.67 was token units counted as hours and 4.68 was euro amounts counted as hours.
Two causes, both in the data source rather than the reporting logic, which is why reading the code did not catch them:
- The reporting endpoint we were using returns time records and expense rows together, and our code summed the
valuefield of every row without checking its type. An expense of 4.80 euro was counted as 4.80 hours. - The same endpoint does not return the job type id. So even with the types separated in the data model, the report could not tell a token record from a human hour. A value of 0.61 on a token record means units. On a human record it means hours. Same column, two meanings, no way to distinguish them at read time.
The cost of this was not theoretical. Token records started on 4 September and a report went out to a client on 7 September, which means a client was shown an overrun that did not exist. That is the single worst outcome available to a project whose entire argument is billing transparency, and it happened because we chose a storage primitive by convenience.
Attempt two and three: the tool fought back, and precision was tied to the wrong number
Two more problems surfaced while the time-record version was still standing, both worth knowing if you are about to try this in any PM tool.
A job type can exist and still be unusable. Creating one over the API worked, with the rate set and the archived flag false. Writing a record against it returned 400 "Job type not available." The type has to be switched on through the interface and no API surface reports that state. Worse, the restriction turned out to be per user, not per project: the same job type accepted records for one user id and refused another, while a different type accepted both. Listing a user’s job types showed all three, so the listing does not tell you what will be accepted. The workaround is to write the record as the permitted user and reassign it immediately.
Precision was coupled to the hourly rate. A time record’s value field silently keeps two decimals. We sent 1.6107, the response echoed 1.6107 back, and reading the record returned 1.61. Because the value was hours, the money error depended on the rate the job type carried, and the truncation is invisible at write time. You are choosing your billing precision when you pick a rate, which nobody should be doing by accident.
What we landed on: resource consumption is money, so it goes where money goes
The fix was one sentence once it was obvious. Ask whether the thing you are recording is time or money, and use the primitive that matches. AI consumption is money, so it moved to expenses.
Twelve expense categories now carry it, bound to the individual task rather than the project, in euro. On a task the Time field is human hours and nothing else; Cost Summary is resource cost. A client reading a task sees two numbers that mean two different things, which is what the whole exercise was for.
An expense has no quantity field, only an amount, so quantity lives in the description. The format is mandatory and checked:
PREFIX · item · quantity units × rate = amount · what the work was
TOKEN · Token Usage (Opus) · 0.61 units × 3.00 EUR = 1.83 EUR · Preparing the content guide
Living in text does not actually lose the quantity, and this matters more than it sounds. Amount divided by unit rate returns it exactly, so the weekly report recomputes it instead of parsing the sentence. A report that parses prose breaks the first time somebody edits a description. One that divides two numbers does not.
One rule in that format exists purely because we tested it in the wrong order. The task name must not appear in the description. ActiveCollab builds the invoice line itself, as task name: expense description (date). Put the task name in the description as well and the client reads it twice on the same line. We found this after applying the format to sixty records, because the format was designed against the data model rather than against a rendered invoice. Sixty corrections is a cheap lesson, but it was an avoidable one: generate one line on a real invoice, look at it, then roll out.
Two of the three traps from the time-record version simply disappeared. An expense category created over the API is usable immediately, with no interface step and no per-user restriction. And precision improved rather than needing a workaround, because the value is now money: two decimal places of euro is exactly what an invoice wants, and it is no longer downstream of a rate you picked for other reasons.
The migration itself was fifty-eight records, 86.51 euro across two accounts, and one detail saved it: write the expense, confirm it, and only then trash the time record. On the first pass fourteen of fourteen failed, because we sent the record date as an epoch number and the API answered 500 with “An unexpected error occurred”, which tells you nothing. It wants a plain YYYY-MM-DD string. Thanks to the ordering, not one time record was lost. Trashing first would have made that afternoon a data recovery job.
The old token job types were archived afterwards so nothing gets filed against them by habit. One thing to know: deleting a job type or an expense category returns a success code but only sets the archived flag, and a trashed time record comes back too. Wrong cleanup is recoverable, which is a good property of the tool and a bad reason to be careless.
The rule that stops the catalogue fracturing every time a provider appears
This is the principle the catalogue rests on, and the part I would steal from someone else’s write-up. Each line item’s unit is the provider’s own billing unit. Tokens stay tokens, GPU seconds stay GPU seconds, minutes of audio stay minutes of audio. Charge is cost times two, and cost is the provider’s list price for that unit.
That gives twelve rows: three token tiers, then image generation per ten images, video per ten seconds, GPU compute per hundred GPU seconds, speech to text per sixty minutes, machine translation per million characters, web crawl per thousand pages, data API per thousand queries, and media CDN per hundred gigabytes. The unit goes inside each row’s name, because the tool gives a category no description field and whoever files a record has to know what they are entering from the name alone.
The twelfth row is the one that does the real work. It is called External API Cost and its unit is one euro of provider cost, charged at two euro. It is the catch-all for anything not in the catalogue. A new service appears, you put the provider’s invoice amount in the value, and the system does not crack. Without that row, every new tool becomes a schema change and a conversation.
Being straight about the current state: only the three token rows are live. The other nine carry costs estimated from public list prices and have not been corrected against a real provider invoice yet, because we have not billed them. When one is used for the first time, the figure gets checked against what the provider actually charged and adjusted. A buyer is entitled to know which numbers on a rate card have been tested and which have not.
One question is still open. On small items the amount is almost comic: twenty-five minutes of transcription comes out at 0.32 euro. A line that costs more in attention than in money is arguably worse than no line. A minimum charge per line, something like one euro, is under discussion and undecided.
The unit got tested five days after we published it
Part three argued that denominating an invoice in units rather than dollars keeps model price changes from becoming client conversations. That claim got an unusually fast test.
Part three went out on 17 September with Opus 5 at five dollars per million input tokens and twenty-five per million output. On 22 September, five days later, Anthropic launched Opus 5.5 at four and twenty, describing it as twenty percent less than Opus 5 and around forty percent cheaper on typical workloads at default settings. Cache reads went from fifty cents per million to twenty, a sixty percent cut.
Nothing on our side moved. One unit is still 100,000 weighted tokens. Because output prices at exactly five times input on every current tier, a weighted token costs the input price, so the raw cost inside a unit went from fifty cents to forty and the billed figure from one dollar to eighty cents. No new rate card, no repricing conversation. The cache read cut is the quiet part: cache reads are overhead we absorb rather than bill, per part two, so a sixty percent drop there improves our margin without touching anybody’s invoice. Worth naming out loud rather than pocketing silently.
What we would do differently, and what you can copy
Three things, in the order they would have saved the most trouble.
- Ask “is this money or is this time” before choosing where it lives. Every problem in the first version came from storing money in a field that meant time. The primitive is the design, not the filing.
- Render one real invoice line before applying a format to sixty records. We validated the description format against the data model, which is the wrong artefact. The client reads the invoice, so the invoice is the thing to test.
- Do not let the separation eat the human hours. One day after the migration the side effect appeared: work finished, expense written, human hour skipped. A task with a cost and no time looks like it was done for free. The sequence is now four steps and it is written down as four: comment, human hours, expense, tag.
If you buy work from anybody metering AI on your account, four questions get you most of the way to knowing whether the invoice is honest. Is AI consumption a separate line from human hours, or are the two added together anywhere in your reporting? What is the unit on each line, and is it the provider’s or one they invented? Which figures on the rate card have been checked against a real provider invoice? And what happens on my invoice the week a model gets twenty percent cheaper?
The last one is the tell. A supplier who cannot answer it either has not thought about it or is hoping the saving stays on their side of the line. Want a second read on an invoice you already receive? Come and talk to us.
Last modified: September 27, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe