- AI
- Backend
- Analytics
LLM cost per user: track OpenAI, Claude and Gemini spend for every user of your AI extension
Measure LLM cost per user for OpenAI, Claude and Gemini: read usage from every response, price it with dated rates, store a ledger and set dollar budgets.
- Author
- Extenify Team
- Published
- Reading time
- 12 min read

On this page
LLM cost per user is the number that decides whether an AI extension or SaaS feature makes money. Your provider invoice tells you what the whole product cost last month. It cannot tell you whether one user ran 4,000 summaries while the median user ran 30, or whether the free plan costs more than the paid one earns. A Hacker News commenter summed up the stakes in 2024: "The per-user revenue of ads is much, much lower than the per-user cost of LLM access." The same is true of a $5 subscription if you do not know what each subscriber costs.
The fix is not a new tool. Every OpenAI, Claude and Gemini response already reports its token usage. This guide shows how to turn those fields into a per-user cost ledger: normalize the three providers' usage objects (they count cached tokens differently), price each request with rates that carry a date, store one row per call, and query cost per user, percentiles and margins. All field names and prices were checked against the providers' documentation on October 13, 2026.
Key takeaways
- Provider dashboards and admin APIs break spend down by project, workspace, API key and model. None of them know which of your users made a request, so you have to record that yourself.
- The three providers count input differently: OpenAI's
input_tokensincludes cached tokens, Claude'sinput_tokensexcludes them, and Gemini'spromptTokenCountincludes cached content. Normalize before you price.- Reasoning and thinking tokens are billed as output on all three. A response can cost money even when it shows no text.
- Prices change on known dates. Gemini 3.8 Flash costs $0.75 / $3.75 per million input and output tokens through December 31, 2026 and $1.50 / $7.50 from January 1, 2027. Price each request with the rate in force that day.
- Store cost as integer micro-dollars per request, then enforce dollar budgets per user, not just request counts.
Why provider dashboards cannot show LLM cost per user
Developers have asked for this for years. The OpenAI Developer Community thread How to track API usage and cost by API key? opened in April 2023 with a one-line request: "We'd like to track usage and cost by API key." The workarounds in the replies are the same ones that work today: log the user, the model and the token counts for every request in your own database, then sum them.
Provider reporting is built around your account structure. Anthropic's Usage and Cost Admin API, for example, lets you filter and group by API key, workspace, model, service tier and context window. That is useful for checking the invoice, but your extension has one API key on one server, and every user's requests flow through it.
The per-user fields that exist do something else. OpenAI's safety_identifier is "A stable identifier used to help detect users of your application that may be violating OpenAI's usage policies." Anthropic's metadata.user_id is "An external identifier for the user who is associated with the request" that "Anthropic may use ... to help detect abuse." Send them (hashed, as both providers ask), but do not expect a cost report keyed by them.
Read usage from every response
Each provider returns a usage object with every non-streaming response and at the end of every stream. These are the fields that matter for cost:
| Concept | OpenAI (Responses API) | Claude (Messages API) | Gemini (generateContent) |
|---|---|---|---|
| Input | input_tokens (includes cached and cache writes) |
input_tokens (only tokens after the last cache breakpoint) |
promptTokenCount (includes cached content) |
| Cache reads | input_tokens_details.cached_tokens |
cache_read_input_tokens |
cachedContentTokenCount |
| Cache writes | input_tokens_details.cache_write_tokens |
cache_creation_input_tokens |
not reported per request |
| Output | output_tokens |
output_tokens |
candidatesTokenCount |
| Reasoning | output_tokens_details.reasoning_tokens (part of output_tokens) |
output_tokens_details.thinking_tokens (part of output_tokens) |
thoughtsTokenCount (separate from candidatesTokenCount) |
Three differences catch almost everyone:
- Claude's
input_tokensis not the whole input. Anthropic's prompt caching docs say it "represents only the tokens that come after the last cache breakpoint", and give the formulatotal_input_tokens = cache_read_input_tokens + cache_creation_input_tokens + input_tokens. - OpenAI's
input_tokensalready contains the cached part. OpenAI's prompt caching guide computes ordinary input asinput_tokens - cached_tokens - cache_write_tokensbefore applying cache multipliers. - Thinking costs output. OpenAI's reasoning guide says reasoning tokens are billed as output tokens and warns you "could incur costs for input and reasoning tokens without receiving a visible response." Anthropic's extended thinking docs describe
thinking_tokensas the share of "the billed output tokens" that "were internal reasoning." Google's thinking docs say "response pricing is the sum of output tokens and thinking tokens."
A normalizer that turns all three into one shape:
// usage.mjs: one usage shape for OpenAI, Claude and Gemini.
export function normalizeUsage(provider, u) {
switch (provider) {
case 'openai': {
// Responses API: input_tokens includes cached and cache-write tokens.
const cachedRead = u.input_tokens_details?.cached_tokens ?? 0;
const cacheWrite = u.input_tokens_details?.cache_write_tokens ?? 0;
return { uncached: u.input_tokens - cachedRead - cacheWrite, cachedRead, cacheWrite, output: u.output_tokens };
}
case 'anthropic':
// input_tokens excludes cache reads and writes.
return {
uncached: u.input_tokens,
cachedRead: u.cache_read_input_tokens ?? 0,
cacheWrite: u.cache_creation_input_tokens ?? 0,
output: u.output_tokens,
};
case 'gemini': {
// usageMetadata: promptTokenCount includes cached content; thoughts are billed as output.
const cachedRead = u.cachedContentTokenCount ?? 0;
return {
uncached: u.promptTokenCount - cachedRead,
cachedRead,
cacheWrite: 0,
output: (u.candidatesTokenCount ?? 0) + (u.thoughtsTokenCount ?? 0),
};
}
default:
throw new Error(`Unknown provider ${provider}`);
}
}
If you use Google's newer Interactions API instead of generateContent, the token counting docs name the fields total_input_tokens, total_output_tokens, total_thought_tokens and total_cached_tokens; map them the same way.
Capture usage from streams
Most AI extensions stream, which is where per-user tracking used to break. In the 2023 OpenAI thread, a developer noted that cost tracking was "Especially important for chat streaming responses which don't include a token count." Today all three providers report usage at the end of a stream:
- OpenAI Responses API: the
response.completedevent carriesresponse.usage. - Claude:
message_startreports input and cache tokens, and the streaming docs say the token counts inmessage_delta"are cumulative", so keep the last one rather than adding them up. - Gemini: streamed chunks are
GenerateContentResponseobjects withusageMetadata; keep the last one you receive.
Record usage in a finally block, so a stream the user cancels halfway is still charged to them. Our guide to streaming AI responses in a Chrome extension shows where that block sits in a Manifest V3 setup.
Price each request with dated rates
Token counts become money through a price table. Two rules keep it correct: prices belong to a date range, and some models charge a higher rate for the whole request once the prompt passes a size threshold. A table with rates copied from the OpenAI, Claude and Gemini pricing pages on October 13, 2026:
// prices.mjs: USD per 1M tokens. Each model has periods; the latest one
// whose `from` is not after the request date applies.
export const PRICES = {
'gpt-6.1-sol': [{
from: '2026-10-13', input: 2.0, cachedInput: 0.1, cacheWrite: 2.5, output: 10.0,
// OpenAI long context: more than 272K input tokens.
tier: { overInputTokens: 272_000, input: 4.0, cachedInput: 0.2, cacheWrite: 5.0, output: 15.0 },
}],
'claude-sonnet-5-5': [{ from: '2026-10-13', input: 2.0, cachedInput: 0.1, cacheWrite: 2.5, output: 10.0 }],
'claude-haiku-5-5': [{
from: '2026-10-13', input: 0.1, cachedInput: 0.01, cacheWrite: 0.125, output: 0.5,
// Prompts over 100,000 tokens.
tier: { overInputTokens: 100_000, input: 0.5, cachedInput: 0.05, cacheWrite: 0.625, output: 2.5 },
}],
'gemini-3.8-flash': [
{ from: '2026-10-13', input: 0.75, cachedInput: 0.075, cacheWrite: 0, output: 3.75 },
{ from: '2027-01-01', input: 1.5, cachedInput: 0.15, cacheWrite: 0, output: 7.5 },
],
};
export function costMicros(model, day, n) {
const periods = PRICES[model];
if (!periods) throw new Error(`No price for ${model}`);
const period = periods.findLast((p) => p.from <= day) ?? periods[0];
const totalInput = n.uncached + n.cachedRead + n.cacheWrite;
const r = period.tier && totalInput > period.tier.overInputTokens ? period.tier : period;
const usd =
(n.uncached * r.input + n.cachedRead * r.cachedInput + n.cacheWrite * r.cacheWrite + n.output * r.output) /
1_000_000;
return Math.round(usd * 1_000_000); // integer micro-dollars: no floating point drift in sums
}
A few notes on the numbers. Claude's cache write rate above is the 5-minute one; 1-hour writes cost more, and the usage object's cache_creation breakdown tells you which you paid for. Built-in tools are billed on top: OpenAI's pricing page lists web search at "$10.00 / 1k calls" plus search content tokens. And the response's model name may be a dated snapshot, so map it to your price key before the lookup.
To see why per-request pricing matters, take a page summary with 6,000 input tokens, 4,000 of them cached, and 500 output tokens with no reasoning. On gpt-6.1-sol it costs $0.0094. On gemini-3.8-flash it costs about $0.0037 today and about $0.0074 from January 1, 2027. A user who runs 40 of those a day for 30 days costs roughly $11.28 a month on the first model and $4.41 on the second, until the Gemini price doubles. If you sell a $5 plan, that one line is your margin.
Write a usage ledger
Store one row per model call. A MongoDB example; the same shape works in Postgres:
// ledger.mjs
import { normalizeUsage } from './usage.mjs';
import { costMicros } from './prices.mjs';
export async function recordUsage(db, { userId, feature, provider, model, usage, status }) {
const at = new Date();
const tokens = normalizeUsage(provider, usage);
await db.collection('llm_usage').insertOne({
userId, // your verified user ID, never an email
feature, // 'summarize', 'chat', 'rewrite'...
provider,
model,
at,
tokens,
costMicros: costMicros(model, at.toISOString().slice(0, 10), tokens),
status, // 'completed', 'incomplete', 'cancelled'
});
}
// Index for per-user queries: db.llm_usage.createIndex({ userId: 1, at: 1 })
The userId must come from a sign-in your server verified, not from a value the extension sends, because anyone can edit extension code. Our guide to Google sign-in in a Chrome extension shows one way to get it, and the backend proxy guide explains why every model call should go through your server in the first place. Send a hash of the same ID as safety_identifier or metadata.user_id.
If you already keep daily store metrics in MongoDB, the patterns in our post on MongoDB time series collections apply here too: raw rows for a few months, daily rollups forever.
Turn the ledger into LLM cost per user
With the ledger in place, cost per user is one aggregation. This one returns the monthly total, the number of active users, the median and the 90th and 99th percentiles, using MongoDB's $percentile operator (MongoDB 7.0 and newer):
db.llm_usage.aggregate([
{ $match: { at: { $gte: ISODate('2026-09-01'), $lt: ISODate('2026-10-01') } } },
{ $group: { _id: '$userId', cost: { $sum: '$costMicros' }, calls: { $sum: 1 } } },
{
$group: {
_id: null,
users: { $sum: 1 },
totalMicros: { $sum: '$cost' },
percentiles: { $percentile: { input: '$cost', p: [0.5, 0.9, 0.99], method: 'approximate' } },
},
},
]);
Read it in this order:
- Average cost per active user (
totalMicros / users) against average revenue per user. This is your gross margin on the AI feature. - Median against the 99th percentile. If the p99 user costs fifty times the median, a flat plan is subsidizing a few people, and a fair-use limit is overdue.
- Cost per feature. Group by
featureas well. One expensive feature with little use is a candidate for a cheaper model. - Free against paid. Group by plan. Free users with no limit can quietly cost more than paying users bring in.
Set budgets in dollars, not requests
Request quotas protect you from bursts, which our guide to LLM API rate limits covers. They do not protect you from cost, because one request with a 200,000-token page costs far more than one with a short paragraph. Check a monthly dollar budget before each call:
// budget.mjs
export async function underBudget(db, userId, limitMicros) {
const monthStart = new Date();
monthStart.setUTCDate(1);
monthStart.setUTCHours(0, 0, 0, 0);
const [row] = await db
.collection('llm_usage')
.aggregate([
{ $match: { userId, at: { $gte: monthStart } } },
{ $group: { _id: null, spent: { $sum: '$costMicros' } } },
])
.toArray();
return (row?.spent ?? 0) < limitMicros;
}
At higher volume, keep a running monthly counter per user instead of summing on every call. Then act on what the ledger shows:
- Route by cost. Send short, simple tasks to a smaller model and reserve the large one for the requests that need it.
- Cache the shared prefix. System prompts and tool definitions are the same for every user; our guide to prompt caching shows the savings and the break-even math.
- Alert on spikes. Post a daily summary and any user above a threshold to your team channel. Our guide to Discord webhooks in Node.js has a sender that handles rate limits.
- Plan for price and model changes. Add the new period to the price table the day a change is announced, and watch shutdown dates; our list of LLM model retirements in late 2026 is a good place to start.
Reconcile the ledger with the invoice
Once a month, compare the ledger total for each provider with the provider's own reporting. They will not match to the cent, and small gaps are normal: tool calls you did not record, requests that failed after the provider billed them, or a price you forgot to update. A gap of more than a few percent usually means a missing code path, such as a background job that calls the model without writing to the ledger. Anthropic's Usage and Cost Admin API makes this check scriptable; it requires an Admin API key, and the docs note that "The Admin API is unavailable for individual accounts."
Where store data fits
Cost per user is half of the unit economics. The other half is how many people use the extension and whether that number is growing. Divide monthly AI cost by weekly users from the store and you get a cost per active user you can track release over release, and a sudden jump in cost with flat users points at a change in your own code.
Extenify covers the store side as a chrome extension analytics dashboard: it records users, ratings, reviews and versions for your extension on the Chrome Web Store, Edge Add-ons and Firefox Add-ons every day, next to your competitors, and alerts you when they change. Our guide to the Chrome Web Store stats API explains what those public numbers mean. Every account starts with a 7-day free trial, and the pricing page lists the plans.
Frequently asked questions
How do I track OpenAI API usage per user?
Route every call through your own server, read usage from each response (or from the response.completed event when streaming), and store it with the ID of the signed-in user. OpenAI's dashboard reports usage for your organization and projects, not for the end users of your app, so the per-user view has to come from your own log.
Does the OpenAI user parameter track cost per user?
No. OpenAI's API reference says the user field "is being replaced by safety_identifier and prompt_cache_key." safety_identifier helps OpenAI detect policy violations and prompt_cache_key improves cache hits. Neither produces a cost report.
Do streaming responses include token usage?
Yes, at the end. OpenAI's Responses API includes usage in the response.completed event, Claude reports cumulative counts in message_delta, and Gemini includes usageMetadata in the streamed responses. Record it even when the user cancels.
Are reasoning or thinking tokens billed?
Yes. OpenAI bills reasoning tokens as output tokens, Claude reports thinking as part of its billed output_tokens, and Google prices Gemini responses as output plus thinking tokens. A response can stop for length after spending tokens on reasoning and still be billed.
How much does an AI feature cost per user?
It depends on the model, prompt size, caching and how often people use it, so measure it. As an illustration, a request with 6,000 input tokens (4,000 cached) and 500 output tokens costs $0.0094 on gpt-6.1-sol at October 2026 prices; 40 of them a day comes to about $11 a month for that user.
Summary
Provider invoices show what your AI feature cost; only your own ledger shows LLM cost per user. Read the usage object from every response and stream, normalize the three providers' different input accounting, price each request with rates that carry a date and a size tier, and store integer micro-dollars with a verified user ID. From there, percentiles show who drives your bill, dollar budgets protect your margin, and a monthly reconciliation keeps the numbers honest.
Image credits
- Cover photo: Closeup of the ON button of a financial calculator by asternecky on the WordPress Photo Directory, CC0.


