Skip to main content
/admin/cost is a single-page operational view of how much your AI providers are costing you. It reads the ai_call table (every call your app makes is logged there with token counts, cost in micro-cents, and latency), rolls up the last 30 days, and breaks it down per provider/model. Auth-gated by requireAdmin() in the layout — only admins reach the route.

Prerequisites

  • AI calls already flowing through the provider abstraction (Phase 2). The page is empty until your first call lands.
  • Your user record has role = 'admin'. Set it via SQL or the admin user page.
  • The ai_call table exists — created by pnpm db:push after src/db/ai.schema.ts.
  • Per-model price book entries (otherwise cost shows as 0 — see pitfall 3).

Step-by-step

  1. Sign in as an admin at /login, then visit /admin/cost.
  2. Read the four summary cards at the top:
    • Total calls (last 30d)
    • Total tokens (input + output)
    • Total spend (rendered through <CostBadge>, micro-cents to dollars)
    • Success rate (% of calls with status = 'success')
  3. Drill into the per-model table below. Rows are sorted by spend descending. Columns: provider/model, calls, input tokens, output tokens, cost (CostBadge), avg TTFT (ms), error rate (%).
  4. Spot the offenders — high error rate, slow TTFT, or runaway spend.
  5. Investigate in /admin/orders or by querying ai_call directly with pnpm db:studio.

How the rollup works

The page runs two SQL aggregates against ai_call:
// Summary card totals
const totals = await db
  .select({
    calls: sql<number>`count(*)::int`,
    successCalls: sql<number>`count(*) filter (where status = 'success')::int`,
    inputTokens: sql<number>`coalesce(sum(input_tokens), 0)::int`,
    outputTokens: sql<number>`coalesce(sum(output_tokens), 0)::int`,
    cost: sql<number>`coalesce(sum(cost_micro_cents), 0)::int`,
  })
  .from(aiCall)
  .where(gte(aiCall.createdAt, since));
The per-model table groups by (provider, model) and orders by sum(cost_micro_cents) desc. Both queries window on createdAt >= now() - 30d.

Verify it works

  1. Trigger an AI call from anywhere in the app (e.g. a test prompt).
  2. Refresh /admin/cost — the call count goes up by one, tokens reflect the request/response sizes, and cost is non-zero (assuming the model is in your price book).
  3. Confirm the model appears in the per-model table with the right provider.
  4. If you have multiple providers configured, run a few calls against each; the rows sort by spend so the most expensive model bubbles up.

Common pitfalls

  1. Empty dashboard until you make AI calls. New install? Of course it’s empty. The page only reads from ai_call — no calls, no rows.
  2. Cost shown as 0. Your cost_micro_cents column is 0 because the model has no entry in the price book. Add it under src/ai/pricing.ts (Phase 2) — or accept the discrepancy if you only care about token counts.
  3. Micro-cents math. Stored cost is in micro-cents to avoid floating- point errors. <CostBadge> divides by 1,000,000 to render dollars. If you query the table directly, remember to do the same: cost_micro_cents / 1_000_000.0 for USD.
  4. Slow queries on large datasets. Past ~1M ai_call rows the per-model group-by gets sluggish. Add a Postgres index on (created_at, provider, model) or partition by month.
  5. 30-day window is hardcoded. Want a different window? Edit WINDOW_MS at the top of src/app/[locale]/(app)/admin/cost/page.tsx. There’s no query-string toggle yet.

Official docs