CortexDB Docs
Operations

Token & Cost Ledger

GET /v1/admin/usage (v0.9.12+) — durable per-role token and dollar accounting, what it measures exactly, what it only bounds, and what it refuses to price.

From v0.9.12 every LLM and embedding call the server makes is recorded in a durable usage ledger at <data_dir>/usage/ledger.json, one bucket per UTC day, pipeline role, tenant and model. The counter that existed before was process memory and reset on every restart; historical spend from before the upgrade cannot be reconstructed, and the ledger does not pretend otherwise. Since v0.9.13 the same data is on the Admin Console's Observability page ("Tokens & cost") and as a month-to-date strip on Overview. Verified live on v0.9.13.

The route

GET /v1/admin/usage?from=2026-09-01&to=2026-09-30          # inclusive days, YYYY-MM-DD
GET /v1/admin/usage?period=mtd                              # month to date; or period=all (since inception). Used only when from/to are absent
GET /v1/admin/usage?role=answer&tenant=org:acme
GET /v1/admin/usage?format=csv                              # flat per-bucket export for a billing pipeline

An unknown period or role is a 400 (INVALID_PERIOD / INVALID_ROLE). Requires operator access: the ledger is process-global and spans every tenant on the node.

{
  "range": { "from": null, "to": null, "observed_from": null },
  "durable": true,
  "total":         { "calls", "failed_calls", "cached_calls", "incomplete_calls", "unpriced_calls",
                     "tokens_input", "tokens_output", "tokens_total", "tokens_unsplit",
                     "cost_usd", "latency_ms_total", "latency_ms_mean" },
  "by_day":        [ { "day": "2026-09-18", ...same counters } ],
  "by_role":       [ { "role": "embedding", ... } ],
  "by_role_model": [ { "role": "embedding", "model": "text-embedding-3-small", ... } ],
  "by_tenant":     [ { "tenant": "org:acme", ... } ],
  "cost_basis":    { "tokens": "measured", "cost_usd": "router_reported", "recompute_from": "by_role_model", "note": "..." },
  "instrumentation": { "by_role": [ { "role", "instrumented", "coverage": "full" | "partial" | "none", "covers" } ] },
  "enrichment_coverage": { "events_enriched", "events_with_facts", "facts_total", "facts_per_event", "events_failed", ... },
  "enrichment_backlog":  { "jobs_pending", "jobs_queued_total", "jobs_completed_total" }
}

The CSV columns are day, role, tenant, model, calls, failed_calls, cached_calls, incomplete_calls, unpriced_calls, tokens_input, tokens_output, tokens_unsplit, cost_usd, latency_ms. A tenant that cannot be attributed is written as __unattributed__.

Roles: embedding, entity_extraction, enrichment, enrichment_qa, recall, answer, verifier, reranker, plus the background lanes (layer_build and the maintenance tick). entity_extraction and recall are two uses of the one CORTEX_LLM_* router.

Read the tokens, then the dollars

by_role_model carries exact token counts beside the model name. That is the durable record; cost_usd is a view over it computed from the server's price book, so you can recompute at a corrected rate, or at your own rate if you bring your own model, without re-running anything. Set CORTEX_MODEL_PRICES to give an unlisted model a rate.

Pricing before v0.9.12 was wrong; re-read anything you quoted

The enrichment lane billed every model at the cheapest model's rate (roughly seventeen times understated if you ran gpt-4o there), anything whose name contained opus was billed at the 4.0 rate (roughly three times overstated for claude-opus-4-6), and an unrecognised model silently took Sonnet's price. One price book now serves every lane, and an unknown model is reported rather than guessed.

Three marks so the report cannot flatter itself

FieldMeaning
unpriced_callsThis build has no rate for that model. The tokens are exact; the dollars are zero because unmeasured, not because free. The UI shows a hatched "unpriced" chip, never $0.00.
tokens_unsplitThe provider reported one combined number instead of an input/output split (the entity-extraction lane does this). Kept separate on purpose: pricing it at an output rate would overstate that lane several-fold.
incomplete_callsWhile non-zero, the token fields are a floor ("≥ n" in amber in the UI).

And two roles are declared uninstrumented rather than read as zero: instrumentation.by_role reports coverage: full | partial | none per role. The reranker records nothing (its interface carries no token or cost data and its provider bills per search unit), and four small fact-join calls inside the answer path discard their usage before it can be recorded, so the answer lane is partial. A window with no answer calls therefore reads "this lane was idle", not "no dollars will ever be recorded"; v0.9.13 fixed a UI bug that conflated the two. Lanes pointed at an operator-controlled endpoint (CORTEX_RERANKER_URL, a local model) report 0.0 cost by design.

Enrichment coverage

The same payload carries enrichment_coverage (events enriched, events with facts, facts per event, failed) and enrichment_backlog (a live gauge of pending jobs), so "is enrichment keeping up?" and "what does it cost?" are one call. See Self-hosting defaults for what enrichment is.

On this page