Per-tenant AI token usage and cost

Every Gemini call now writes one row into our own database: how many tokens it used, which model answered, and which API key paid for it. Three pages read those rows and show tokens plus an estimated dollar cost, per tenant, per feature and per model.

Status as of 20 Sep 2026

Built on branch feat/tenant-ai-token-usage. Nothing is committed yet. There is no Jira ticket and no PR.

Not on beta. Not in production.

The screenshots come from a local stack running this branch. The usage rows are seeded demo data, so the numbers are invented. The screens themselves are real.

What it does

Who sees what

WhoWhereWhat they see
Tenant admin LMS, admin Usage page Their own tenant. All token counts. Cost only for calls paid by their own API key.
Platform admin Control plane, tenant detail, Usage tab One tenant. All token counts and the full cost, split into platform key and tenant key.
Platform admin Control plane, AI usage page Every tenant together. Totals, cost split, and tables by tenant, by feature and by model.

Walkthrough

Captured at 1440 pixels wide with the app sidebar open, against a local build of this branch. Today in the demo data is 20 September 2026.

1. LMS, tenant admin, current month

This is the new block at the bottom of the admin Usage page. Five tiles, then the daily line next to the last six months, then the feature table across the full width. The cost tile says $4.31 because a tenant admin only sees the cost of calls paid by their own key.

LMS admin AI usage section for September 2026 with five tiles, a daily chart, a feature table and a six month bar chart
Look at the grey line under the tiles. Two requests used a model with no price, so the page says the cost is a minimum instead of hiding the gap.

2. LMS, previous month

The month arrows in the page header move the whole page, including this block. Here it is August: 31 days on the daily chart, and the six month bars end at August.

The same AI usage section showing August 2026, with 31 days on the daily chart
March is in the history chart with no bar, because the tenant had no recorded AI usage that month.

3. LMS in Vietnamese

All labels, feature names and number formats follow the language. Tokens become "8,5 Tr", the cost becomes "4,31 US$".

The same AI usage section in Vietnamese
The feature names are translated too, for example "Gia su AI" for the AI tutor. These strings are my own translation and still need a native reader.

4. Control plane, one tenant

Same tenant, same month, seen by a platform admin. The full cost is $13.83. Under the main tiles, two tiles split it: $9.52 paid by the platform key and $4.31 paid by the tenant key. That $4.31 is the number the tenant admin sees in screenshot 1.

Control plane tenant detail, Usage tab, AI usage block with cost tiles, a daily chart, a feature table and a six month bar chart
Reading illustration is the interesting row: 44 requests, only 117,921 tokens, and $3.93. Image tokens are expensive.

5. Control plane, every tenant

A new AI usage entry in the left nav opens the platform-wide page. Same shape as the tenant block, but the numbers cover all tenants: 16.1M tokens and $25.64 for September.

Control plane AI usage page, top half, with the nav entry selected, five tiles, the cost split and the daily chart
The caption under the tiles is the honest part: cost is our own estimate, not a provider invoice, and 11 requests used a model we have no price for.

6. The three tables

By tenant is sorted by tokens, and each row links to that tenant. By feature and by model sit below it, each across the full width.

By tenant, by feature and by model tables on the control plane AI usage page
In the by-model table, gemini-experimental-x shows a dash for cost, not $0.00. The failed calls sit on gemini-flash-latest with zero tokens and $0.00, which is correct: a call that burned nothing costs nothing.

7. Dark mode

The control plane AI usage page in dark mode
The control plane already has a theme switch, so the new page inherits it. Nothing special was needed.

How usage is recorded

How cost is calculated

We keep our own price table in the code. Each entry names a model and a start date, so a past month keeps the price that applied back then.

Here is the sum in words. Fresh input tokens are charged at the input rate. Tokens that came from cache are charged at the cheaper cache read rate, and they are subtracted from the input count so nothing is billed twice. Output tokens are charged at the output rate, and thinking tokens go in with them because Google bills thinking as output.

Nothing is stored. The cost is worked out when someone opens the page. If we find a wrong price and fix the table, old months repair themselves on the next page load.

Gemini 3.8 Flash, USD per 1 million tokens

PeriodInputCached inputOutput, thinking included
Until 31 Dec 2026$0.75$0.075$3.75
From 1 Jan 2027$1.50$0.15$7.50

The 2027 prices are already entered. January will price itself correctly with no code change, and December stays on the old price.

Images are the expensive part Course refresh illustrations use gemini-3.1-flash-image. Its output is image tokens, priced at $60 per 1 million. In the demo month that is 44 requests, 118 thousand tokens, and $3.93. The study buddy made 267 requests for $0.60.

A model with no entry in the table shows "Unknown" or a dash, never $0.00. The page also counts how many requests had no price, so a number you see is a floor, not a guess.

Who pays

Quiz import from documents runs on the tenant's own Gemini key. Everything else runs on our platform key. The tenant admin page prices only the tenant's own key, which is why the same tenant reads $4.31 in the LMS and $13.83 in the control plane. Both numbers are right, they answer different questions.

PostHog also estimates a cost for these calls. We treat it as a cross-check on our own table, not as the source.

Limits

This is an estimate, not an invoice. Compare it with the provider bill before you bill anyone.

Four things are outside the table, so wherever they apply the real bill is higher:

Checks run

Backend: 1699 unit tests pass Backend: lint and format clean LMS: 416 tests pass LMS: build passes Control plane: lint, types and build pass

What I did not check

Layout fixes after the first pass

The first screenshots showed the Cost column pushed out of its card. Every table that carries a cost now gets a full-width row, in the LMS and in the control plane, so the column reads at 1440 pixels in English and in Vietnamese with the sidebar open. The chart axis labels and the "Total tokens" hint were also being clipped, and both are fixed. Every screenshot here was retaken after those changes, with the sidebar open.

Open items