A single OpenAI API key can back a chat feature, a summarization feature, a search re-ranker, and an internal admin tool simultaneously. The provider's usage dashboard shows one number: total spend on that key. When the bill jumps 40% in a week, that dashboard cannot tell you whether it was the chat feature getting more traffic or the summarizer processing larger documents — you find out by grepping application logs, if you logged the right thing in the first place.
Why per-API-key visibility isn't enough
Most teams start with one API key per environment (or per provider), which is the right call for secret management but the wrong granularity for cost attribution. The provider's billing page aggregates at the key level because that's the unit it authenticates — it has no concept of your product's feature boundaries. Splitting into one key per feature technically works but scales badly: key rotation, rate limit pooling, and secret storage all get harder with every key you add, and you still don't get spend broken down by user within a feature.
The fix is attribution at the application layer: tag every LLM call with metadata about which feature triggered it, log that alongside the actual cost, and aggregate afterward. This works with a single API key because the attribution lives in your own data, not the provider's.
Tagging calls with feature metadata
Use a fixed vocabulary for feature. Free-text feature names drift ("summary", "summarize", "thread-summary") and fragment your aggregates. A TypeScript union or an enum forces the names to stay consistent:
type Feature = "chat" | "summarization" | "search_rerank" | "admin_tools";
async function trackedCall(feature: Feature, userId: string, request: LLMRequest) {
const start = Date.now();
const response = await llmClient.call(request);
await db.insert("llm_events", {
feature,
user_id: userId,
model: request.model,
tokens_in: response.usage.prompt_tokens,
tokens_out: response.usage.completion_tokens,
cost_usd: computeCost(request.model, response.usage),
latency_ms: Date.now() - start,
created_at: new Date(),
});
return response;
}Every call site passes its feature tag explicitly rather than the tracking logic trying to infer it — inference from context (which endpoint called this, which module imported it) breaks the first time code gets refactored, while an explicit tag at the call site stays correct because it's part of the function signature.
Aggregating and surfacing the breakdown
Once every call is tagged and logged, feature-level cost is a GROUP BY:
SELECT feature, SUM(cost_usd) as total_cost, COUNT(*) as calls
FROM llm_events
WHERE created_at > now() - interval '7 days'
GROUP BY feature
ORDER BY total_cost DESC;This is the query that answers "why did the bill jump" in seconds instead of a log-grepping session — pull the same query for the prior week and diff the two, and the feature responsible for the increase is immediately visible rather than inferred from provider-level totals.
The same tagging pattern extends to per-user cost within a feature (add user_id to the GROUP BY), which is the same data you need if you're planning to charge customers for their usage rather than just monitor internal cost — attribution and billing turn out to be the same underlying problem viewed from two angles.
Where noburn fits Free-text feature names drift ("summary", "summarize", "thread-summary") and fragment your aggregates. A TypeScript union or an enum forces the names to stay consistent:
Where noburn fits
The tools compared in this article handle observability, routing, or evaluation — all of which operate after the LLM call completes. noburn operates before it. It wraps your existing openai client, estimates the token cost of each call, and blocks it if the calling user or project has exceeded their budget. Nothing in this comparison does that at a self-serve price point.
Per-user metering lets you enforce separate limits per end-customer, and Stripe passthrough lets you bill them for their LLM usage without writing a billing layer yourself. The free tier covers 100 requests per month. Documentation and SDKs are at noburn.dev/docs.