Playbook

Tracking AI Spend Across Providers and Finding $8,000 a Year on the Floor

A leader asked what we were actually spending on AI. Nobody could answer. The lump sum on the card had quietly become a real line item, and the most important question (is any of it wasted?) was unanswerable.

The situation

We got here in a panic. "Everyone else is ahead of us" became the loudest thing in the room, and it turned into a scramble: seats handed out to anyone who asked, no time spent understanding the tools or building a strategy, everything aimed at catching up and nothing aimed at getting it right. I came in afterward as the cleanup crew.

"What are we spending on AI?" sounds like a question you should be able to answer in a meeting. It isn't. Our spend lives across several providers, and no two bill the same way: one charges flat annual per-seat fees plus metered overage from a prepaid credit balance; another bills per token by model; a third is a flat power-user subscription on top of the others. Finance could see one number (a lump sum on a card, going up) and nothing underneath it: not who, not on what, not whether any of it was wasted.

The constraint

The data to answer the question existed, but every provider gates it differently. One exposes a proper admin API. One gives you a billing page and PDF invoices. At least one shows per-person usage only inside your own logged-in browser session. There was no one place to look, and no budget or mandate for a procurement function. Just me, evenings, and conversational AI-assisted building.

What I built

A cost lens, not an accounting system: an instrument for decisions, not for closing the books. Four goals: one view across every provider; cost attributed per person ("we spend a lot" is not actionable; "these six people drive most of it" is); reconciled to the real bill; waste surfaced. Explicit non-goals: not billing-grade, not real-time, not automated procurement.

The design principle: use the best source each provider will give you, and be honest about which is which.

  • Where there's an admin API, the dashboard pulls live: usage and cost by workspace, model, and person.
  • Where there isn't, seat counts and prices live in a small config, usage comes from downloadable analytics exports, and dollars get confirmed against the PDF invoices.
  • Where even that fails, I read the numbers straight out of my own logged-in browser session. That started as a save-the-network-log-and-parse-it hack and has since become a small extension, which is its own section below.

Everything normalizes to dollars per person per month, grouped by team. The piece I'm most glad I insisted on is reconciliation: every person's attributed cost must sum back to the actual invoice, with the leftover shown explicitly as shared infrastructure and idle seats. When the per-person view and the bill agree, you can trust the per-person view.

The view that does the actual work

All of that plumbing exists to produce one screen: every person in the company, on one row, across both providers, sorted by what they cost. Not a script I run and paste into a spreadsheet. A page I open.

A dashboard table listing people with their seat cost, metered usage, and monthly total across two AI providers, with summary tiles above it.
The per-person cost view, rebuilt as a mockup. People are shown as role labels rather than names, every number is invented, and the org's own totals are deliberately not shown. The layout, the columns, and the staleness flag are the real ones.

The columns are the design. Seat cost is flat and known. Metered usage is neither, so it gets its own column per provider, run-rated to a monthly figure from a 30-day window, with a 7-day toggle for when I want to see a spike rather than an average. Two columns that get added together elsewhere stay separate here, because the whole point is that one of them is the one that moves.

The affordance I did not expect to care about, and now would not build without: the view tells you how much to trust each number. Its own header says which figures have no live API and are refreshed by hand. Any figure more than 21 days past its last refresh gets a warning marker on the row. Everything else is live or recent. That is a contract, not a disclaimer. A dashboard where every number looks equally authoritative teaches people to trust the stale ones exactly as much as the fresh ones, and the first time somebody acts on a three-week-old figure in front of finance, the whole tool is done.

Filters, in order of how often I use them: on both providers, on one, on the premium tier, on a team plan plus the premium tier. Plus a plain name filter, because half of what I actually do here is answer "what is this one person's setup" during a renewal conversation.

Judgment calls that mattered more than the plumbing:

  • Per person, not totals. Totals lie. Our biggest raw bill looked like our least efficient tool until I divided by headcount. It flipped to cheapest per seat by a wide margin. Cost per seat is the honest lens, and it reversed the conclusion.
  • Every figure carries its provenance and as-of date. Live and hand-refreshed data are labeled as such. A dashboard that quietly goes stale is worse than one that admits it's three weeks old.
  • Estimates labeled as estimates, inputs shown. A modeled number presented as fact is how you lose the room the first time finance checks your math.
  • Room for human context. A seat can look redundant on paper and be impossible to remove. Annotations silence the "this looks wasteful" prompt for decisions already made. A cost tool that nags about settled decisions is one you stop opening.

The refresh chore, and the button that ate it

Here is the part nobody warns you about. A cost lens that depends on manual exports is a standing chore, and the chore is what kills it. Mine had grown to this, per refresh, per account:

The ritualWhat it took
Member list, one providerExport CSV, upload it. Once per account.
Spend limits and overageOpen the usage page, scroll it all the way down so every request fires, save the whole network log, upload that.
Per-user code metricsOpen the raw JSON response by hand, copy, paste.
The other provider's analyticsExport JSON from four separate charts, then two more from the per-user leaderboards, upload all six.
Seats in useRead the number off the screen and type it in.

Multiply by the number of accounts. That is why the data went stale, and staleness is the failure mode that makes a cost tool worthless quietly rather than loudly.

So the refresh became a browser extension with one button. What matters about it is not that it exists, it's what it does instead of scraping:

  • It calls the providers' own admin endpoints, from inside my logged-in session. Every one of those manual rituals above was a human doing the work of an HTTP request that the provider's own admin console already makes. The member list, the spend limits, the per-user code metrics, the analytics behind each chart, the seat counts: all of them are JSON endpoints, and you can watch your own browser call them with devtools open. No CSS selectors, no parsing rendered HTML, nothing that breaks when they change a font.
  • It never touches a credential. It reuses the session from a normal signed-in tab. There is no password, no token, and no secret in the extension, which is the only version of this idea I would be willing to run against a company admin account.
  • Multiple admin logins, using browser containers. Different accounts need different admin identities. One container per login, one hidden tab per container, discover every org that login can administer, skip the ones it can't on the permission error, dedupe across containers, post one bundle at the end.
  • Partial syncs are safe by design. The server merges by coverage and never wipes an account that wasn't in this batch, and a freshness stamp only advances for what actually synced. That is what makes the staleness flag in the view above trustworthy: a half-finished sync shows up as half-fresh data, not as a silent overwrite with holes in it.
  • An org it doesn't recognize is loud. Unmapped accounts come back as a warning in the sync result. The failure I was most afraid of, a new team quietly missing from the numbers for a month, is the one I made impossible to miss.
  • The manual uploaders are still there. Some things it doesn't cover: invoice PDFs, contract terms, seat prices that change only at renewal. Those stay manual on purpose, and the old upload path stays as a fallback.

I'm not publishing the code. It is welded to my employer's accounts and the org structure behind them, and a stranger's copy of it would be a liability rather than a gift. The useful thing to hand over is the shape of the ask, because the whole thing was built conversationally:

Build me a Firefox extension that refreshes my AI cost dashboard in one click.

Rules:
- Call the providers' own admin JSON endpoints from my already-signed-in
  session. Do not scrape the DOM and do not ask me for credentials or
  API keys. Open devtools on each admin page, find the XHRs behind the
  numbers I care about, and use those.
- Support more than one admin login using browser containers: one
  container per login, one hidden tab each. Discover every org that
  login can administer, skip orgs it cannot on a 401 or 403, and dedupe
  orgs seen from more than one container.
- POST one bundle to my dashboard's ingest endpoint, using my normal
  session in a regular tab.
- Merge on the server by coverage. Never delete or zero an account that
  was not in this batch. Advance the freshness timestamp only for the
  accounts that actually synced.
- Return a result readout per org and per dataset, and warn loudly about
  any org the server has no mapping for.
- Leave my existing manual upload path working as a fallback.

Start by listing the endpoints you would call and what each one replaces,
and let me confirm that list before you write any code.

That last line is the one I'd keep. The endpoint list is the whole design, and it is the part you want to argue about while it is still a list.

What it found

  • The cost everyone assumed was small was the dominant one. On one provider the standing assumption was "we just pay for seats." Seats turned out to be a minority of real spend. Metered usage ran several times larger, almost entirely from one coding agent, concentrated in a handful of heavy users. Invisible because nobody had ever put a meter on it.
  • A $200/month charge nobody could explain. The admin API said which key and which model: an expensive top-tier reasoning model with the huge cached-context footprint of an agentic loop. But only the code could say which tool. Grepping my own automation showed the culprit, a background automation I'd long believed ran free on a subscription seat we already pay for. The root cause was a credential-precedence rule in Claude's own tooling, and it's worth calling out on its own because it'll bite anyone in the same setup: when both a metered API key and a subscription login are present, Claude silently prefers the key. No error, no alert, no "you're about to leave your subscription seat idle." Every unattended run had been billing the metered key, on the expensive model, silently. The fix was surgical: strip the key from that one automation's environment for the duration of its run, so it falls through to the subscription. Validated by watching the meter. The expensive model's usage on that key went to zero on run-day while everything that genuinely needs the key kept working.
  • Money on the floor. Unassigned paid seats, people carrying two overlapping plans, a premium tier provisioned for usage that never came. Two ratios I'd look for in anyone's numbers: how much of the bill is neither a person nor a project (mine was roughly 7 percent in shared infrastructure and idle seats), and how many people are paying twice by holding a seat on two providers (under a tenth of the org, which sounds small until you price it).

The numbers

Realized and identified, kept separate on purpose:

SavingStatusBasis
Automation moved off metered key onto existing seatRealized, validated~$150/mo, ~$1,800/yr (undercount, earlier runs predate cost logging)
Unassigned seatsIdentifiedLow-thousands/yr, billed-minus-assigned counts
Overlapping plansIdentified, with exceptionsA few seats' worth/yr
Over-provisioned tiersIdentified, unconfirmed per personMaterial if usage stays low
Total surfaced≈ $8,000/yr, deliberately an undercount

Only the first row is money actually no longer being spent. The rest is a worklist, not savings banked. Capturing it means cutting seats at renewal and confirming tiers, which is work still to do.

The meta-economics: the lens cost a few evenings of conversational building, and the first leak it caught paid for the effort several times over. When software is this cheap to build, a cost tool doesn't need to find a fortune to be worth it.

What I'd do differently

  • Turn on cost logging in my automations from day one. The realized-savings figure is an undercount purely because earlier runs weren't logging their own cost. Cheap to add, impossible to backfill. ("My automations" here means the unattended, scheduled jobs like the background one above that ran on the wrong key for months, not anything interactive.)
  • Never accept "we just pay for seats" without a meter. The dominant cost on one provider was invisible for months because the assumption went unchecked. Metered usage is exactly the spend that can triple quietly.
  • Delete the manual refresh, don't schedule it. My first answer to the staleness problem was a recurring calendar slot, which is the answer that sounds responsible and doesn't work. A refresh I have to remember is a refresh that drifts, and a calendar reminder just moves the drift to whichever week I'm busy. The chore had to stop being a chore. Everything that turned out to be automatable was already an HTTP call I was performing by hand.
  • Expect the price to keep climbing, not the same leak to reopen. I tested the fix and I trust the key it's using now; I don't expect that specific leak to come back. What I do expect is the price itself to keep moving. We've already seen it move once with Fable, and I'd bet Opus 5 goes the same direction. Every provider I track has already repriced at least once since I built this lens, and there's no reason to think that stops: the more dependent everyone becomes on these tools, the more room providers have to charge for them. A cost lens isn't a one-time build. It's something you keep reading.

Take what helps

AI spend is migrating from flat seats to metered usage, and metered usage is invisible until you instrument it. Per-seat is a boring known number. Per-token drawn from an auto-refilling prepaid balance can quietly triple while everyone assumes "we just pay for seats."

If you take one thing: attribution takes both the bill and the code. The API told me which key and model. Only the codebase could say which tool. Neither alone was enough. Build the cheapest lens that reconciles to your real invoice, and follow the first weird number you see. Mine started as a $200 question.

Your providers and plan tiers will differ. Take what helps, skip what doesn't.

Get new playbooks by email

One email when a new playbook or guide goes up, nothing else. Unsubscribe anytime.