Token & Cost Ledger
GET /v1/admin/usage — durable per-role token and dollar accounting, what it measures exactly, what it only bounds, and what it refuses to price.
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
same data is on the Admin Console's Observability
page ("Tokens & cost") and as a month-to-date strip on Overview.
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 pipelineAn 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, and
the one to re-price from: multiply the tokens (or the CSV export) by a corrected rate, or by your own rate
if you bring your own model, without re-running anything. cost_usd is priced from the server's price
book when each call is recorded; changing the rates later does not re-price calls already recorded.
Set CORTEX_MODEL_PRICES to give a model a rate, in USD per million tokens, input/output,
comma-separated:
CORTEX_MODEL_PRICES=gpt-5.6-terra=2/12,gpt-5.6-luna=0.2/1.2,gpt-5.6-sol=4/20,gpt-6-astra=10/50It takes precedence over per-provider cost_in/cost_out in CORTEX_LLM_PROVIDERS, which take
precedence over the built-in table. The built-in table covers the Claude families,
gpt-4o, gpt-4o-mini, gpt-4.1 (and -mini, -nano), gpt-5 (and -mini, -nano) and the
text-embedding-3-* / ada-002 embedding models.
Set prices for GPT-5.6 and GPT-6 models
The built-in table has no gpt-5.6-* or gpt-6-* entries; those models are priced at the gpt-5 rate
($1.25 / $10 per million) unless you give them a rate. Set CORTEX_MODEL_PRICES for every GPT-5.6 and
GPT-6 model you run, as above. See Cost Planning.
The ledger shows the model actually called, not diagnostics.answer_model
The answer lane always calls the configured CORTEX_ANSWER_MODEL; a per-request answer_model is echoed
in diagnostics.answer_model only. The ledger records the configured model, which is the one you are
billed for. To answer with another model, change CORTEX_ANSWER_MODEL.
Three marks so the report cannot flatter itself
| Field | Meaning |
|---|---|
unpriced_calls | This 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_unsplit | The 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_calls | While 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". Lanes pointed at an operator-controlled
endpoint (CORTEX_RERANKER_URL, a local model) report 0.0 cost by design.
Streamed answers are booked in full
A streamed /v1/answer ("stream": true) books its input tokens as well as its output tokens.
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.
Related
- LLM & Answer Generation — the lanes these roles map to.
- Admin Console — Observability → Tokens & cost.
- Cost Planning — recommended models and estimated spend.
- Benchmarking — latency measurement.
Security & Compliance
Encryption, TLS / mTLS, RBAC, rate limits, breach detection, audit, SIEM, data residency, DSAR — every governance knob in cortex.toml.
Cost Planning
What a self-hosted CortexDB costs to run in model spend — recommended models, per-event and per-answer costs, monthly estimates, and how to replace them with your own numbers.