← Blog

How to Build an LLM Cost Tracking Dashboard

2026-10-31 · 5 min read · SubToAPI Team

Building an LLM cost tracking dashboard means capturing token usage and spend data at the point of each API call, storing it in a queryable format, and surfacing aggregates (by model, user, feature, or time period) in a UI that updates in near real time. The core components are: a logging layer that records every request's token counts and cost, a database schema that supports fast aggregation, and a frontend that renders trends and breakdowns.

Most teams underestimate this until a surprise invoice arrives. If you're calling Claude or another LLM directly from your backend, usage data exists only in the provider's billing console, aggregated at the account level with no breakdown by customer, feature, or user. A proper cost dashboard requires you to log usage yourself, at the request level, before it reaches the provider's opaque monthly total.

Step 1: Capture Usage Data at the Request Level

Every LLM API response includes token counts. Anthropic's Messages API returns an usage object with input_tokens and output_tokens on every response, including streamed ones (in the final message_delta event). This is the raw material for cost tracking — you don't need to estimate tokens yourself.

Wrap every LLM call in a logging function:

async function callLLMWithLogging({ userId, feature, model, messages }) {
  const start = Date.now();
  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ model, max_tokens: 1024, messages })
  });

  const data = await res.json();
  const latencyMs = Date.now() - start;

  await logUsage({
    userId,
    feature,
    model,
    inputTokens: data.usage.input_tokens,
    outputTokens: data.usage.output_tokens,
    latencyMs,
    timestamp: new Date()
  });

  return data;
}

The key fields to log for every call:

If you're building on SubToAPI, every response already includes this usage object in a consistent shape regardless of streaming or tool use, so your logging wrapper doesn't need model-specific parsing logic. See /docs/messages for the full response schema.

Step 2: Design a Schema That Supports Aggregation

A flat events table is enough for most dashboards. Don't over-normalize early — you want to run GROUP BY queries fast, not join across five tables.

CREATE TABLE llm_usage_events (
  id BIGSERIAL PRIMARY KEY,
  org_id TEXT NOT NULL,
  user_id TEXT,
  feature TEXT,
  model TEXT NOT NULL,
  input_tokens INT NOT NULL,
  output_tokens INT NOT NULL,
  cost_usd NUMERIC(10,6) NOT NULL,
  latency_ms INT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_usage_org_time ON llm_usage_events (org_id, created_at);
CREATE INDEX idx_usage_feature ON llm_usage_events (feature, created_at);

Compute cost_usd at write time using a pricing lookup table, not at query time. Prices change per model, and historical events should reflect the price that was in effect when the call happened — recomputing later with current prices will silently corrupt your historical totals.

const PRICING = {
  "claude-sonnet-4": { input: 3.00, output: 15.00 }, // per million tokens
  "claude-haiku-4": { input: 0.25, output: 1.25 }
};

function calculateCost(model, inputTokens, outputTokens) {
  const rate = PRICING[model];
  return (inputTokens / 1e6) * rate.input + (outputTokens / 1e6) * rate.output;
}

Step 3: Build the Aggregation Queries

The dashboard itself is just a handful of grouped queries rendered as charts:

-- Daily spend trend
SELECT date_trunc('day', created_at) AS day,
       SUM(cost_usd) AS total_cost
FROM llm_usage_events
WHERE org_id = $1
GROUP BY 1
ORDER BY 1;

-- Cost by feature, last 30 days
SELECT feature, SUM(cost_usd) AS total_cost, COUNT(*) AS calls
FROM llm_usage_events
WHERE org_id = $1 AND created_at > now() - interval '30 days'
GROUP BY feature
ORDER BY total_cost DESC;

-- Top users by spend
SELECT user_id, SUM(cost_usd) AS total_cost
FROM llm_usage_events
WHERE org_id = $1
GROUP BY user_id
ORDER BY total_cost DESC
LIMIT 20;

These three queries cover the 80% case: trend over time, breakdown by feature, and breakdown by heavy users. Add a fourth for model distribution if you're mixing Haiku and Sonnet calls for cost optimization — that breakdown tells you whether a cheaper model would handle some of your volume.

Step 4: Add Alerting, Not Just Visualization

A dashboard you only check manually catches problems late. Add a cron job or scheduled function that checks daily spend against a threshold and fires a Slack webhook or email if it's exceeded:

async function checkDailyThreshold(orgId, limitUsd) {
  const today = await db.query(
    `SELECT SUM(cost_usd) AS total FROM llm_usage_events
     WHERE org_id = $1 AND created_at > current_date`,
    [orgId]
  );
  if (today.rows[0].total > limitUsd) {
    await notifySlack(`Daily LLM spend for ${orgId} exceeded $${limitUsd}`);
  }
}

Run this hourly, not just once a day — catching a runaway loop at 2pm instead of midnight saves real money.

Step 5: Decide Whether to Build This Layer Yourself

If you're calling an LLM provider's raw API directly, you own all of the logging, pricing-table maintenance, and schema work above. That's a reasonable amount of engineering for a team shipping an AI feature as a side concern, but it adds up if cost tracking isn't your core product.

SubToAPI returns usage and cost metadata with every request and gives you a dashboard with per-key and per-team usage out of the box, so you can skip building steps 1–3 for your own internal reporting and focus the custom dashboard work on product-specific breakdowns (per-customer billing, feature-level cost attribution) that a generic provider dashboard won't give you. Check /docs/quickstart to see the request/response shape, or /pricing if you want built-in usage metadata without maintaining your own logging pipeline.

Questions

Do I need to log every single LLM request, or can I sample? Log every request. Token costs vary too much between calls to extrapolate accurately from a sample, and you lose the ability to debug specific expensive calls or attribute cost to specific users.

Should I track cost in USD or in tokens? Store both. Tokens are the stable unit for understanding usage patterns; cost in USD changes whenever the provider updates pricing, so recalculating historical cost from stored token counts and a versioned pricing table is more reliable than storing cost as the only field.

How do I handle cost tracking for streaming responses? Streaming responses still return a final usage object once the stream completes (in the closing event), so log usage after the stream ends rather than trying to count tokens as they arrive. See /docs/streaming for the event sequence.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →