Skip to main content

Usage Statistics API

ChatWalaʻau keeps an append-only record of the tokens it spends. GET /api/usage/summary reads it back and GET /api/usage/export downloads it as CSV. In the app, the same numbers are on the Token Usage Dashboard.

What is recorded

Every turn that calls a model, in every lane:

  • chats in the web app (both ordinary chats and harness agents)
  • Declarative Workflow runs -- one line per node that ran, in the chat and as a Pipeline job (since v0.154.0)
  • Microsoft Teams conversations
  • requests through the OpenAI-compatible API
  • the background passes you never see: chat titles, user memory extraction, agent memory curation, ontology natural-language queries, and Teams meeting summaries

A turn that you interrupt is recorded too, with what it had already spent -- the tokens were billed whether or not you kept the answer.

Workflow runs, node by node

A workflow does not produce one line per run. Every node that calls its agent writes its own line, so you can see which step of a workflow is the expensive one:

{ "lane": "workflow", "kind": "node", "run_target": "Contract Review Flow",
"node": "summarize_step", "agent": "Summarizer", "run_id": "b41e...",
"model_calls": 3, "uncached_input_token_count": 2100, "output_token_count": 480 }
  • run_target is the workflow's name, node the step's id in your YAML, agent the Prompt agent that step invoked.
  • A node inside a loop writes one line each time it runs.
  • run_id ties the lines of one run together, including a run that paused to ask you a question and continued on your answer.
  • A workflow run as a Pipeline job uses lane: "workflow-job", has no chat, and its run_id is the job id.
  • The chat title generated after a workflow's first reply is recorded as a chat-title line carrying the workflow's name in run_target.

In the chat itself, a workflow's reply shows the run total as its in/out label. Click it for the per-node breakdown and the context occupancy of the node that came closest to its model's window.

What is not recorded

  • Demo mode. Its token counts are estimates rather than measurements, so recording them would put invented numbers into real statistics.
  • A model call that fails before the provider reports usage. There is no number to record, in any lane.
Added in v0.154.0

Earlier versions listed Declarative Workflow runs here as not yet instrumented. They are now recorded, per node.

Corrected in v0.145.1

Earlier versions of this page, and the coverage field itself, also listed the framework's context-compaction calls as unrecorded spending. That was wrong. Compaction here trims history by token budget and never calls a model, so it costs nothing to record. A summarizing compaction strategy would -- and none is configured. Overstating a gap tells you to distrust numbers that were accurate.

Every response repeats this in a coverage field. These totals are what the observable work consumed -- not what your account was charged.

Where it lives

# .env
USAGE_DIR=.usage # default

One file per month, one line of JSON per turn:

.usage/2026-09.jsonl

It is a separate directory from your chats on purpose. Deleting a chat, the temporary-chat cleanup, and chat export/import all operate on the session directory, and a spending record has to survive all three.

Nothing is ever deleted from the ledger, and there is no retention setting.

Reading it

curl -s -H "Authorization: Bearer $API_KEY" \
"http://localhost:8000/api/usage/summary?from=2026-09-01&to=2026-09-30&group_by=day"
ParameterValuesDefault
from / toYYYY-MM-DD, inclusive, in tzthe last 30 days
tzan IANA time zone, e.g. Asia/TokyoUTC
group_byday, month, chat, model, lane, run_target, node, agent, provider, outcome, kindday
seriesa second grouping key from the same list (not the same as group_by)none
lanespa-prompt, spa-harness, workflow, workflow-job, teams, openai-api, helperall

group_by=node keys each group as "<workflow name> / <node id>", because the same step id can appear in different workflows. group_by=run_target groups workflows (and harness agents) by name.

Days in your own time zone (v0.157.0)

The ledger stores UTC. Pass tz and from, to and the day / month keys become dates in that zone -- a record written at 2026-08-31T20:00Z counts toward 2026-09-01 with tz=Asia/Tokyo. Without tz everything is UTC, exactly as before.

Two axes at once (v0.157.0)

series splits every group again. group_by=day&series=model returns each day with a series list, one row per model; the rows always add up to their day. Every group, row and totals also carries first_ts / last_ts, the first and last activity in it.

{
"from": "2026-09-01",
"to": "2026-09-30",
"tz": "UTC",
"group_by": "day",
"groups": [
{
"key": "2026-09-04",
"records": 128,
"model_calls": 402,
"uncached_input_token_count": 51200,
"cache_read_input_token_count": 230400,
"output_token_count": 15360,
"reasoning_output_token_count": 6400
}
],
"totals": { "records": 128, "model_calls": 402 },
"skipped_lines": 0,
"coverage": "Observable model calls only. ..."
}

Exporting for BI (v0.157.0)

curl -s -H "Authorization: Bearer $API_KEY" -o usage.csv \
"http://localhost:8000/api/usage/export?from=2026-09-01&to=2026-09-30&tz=Asia/Tokyo"

The export is the raw record set -- one row per ledger line -- so your BI tool can build any pivot. Parameters are from, to, tz and lane as above, plus format=csv (the only format).

ts, ts_local, lane, kind, purpose, thread_id, temporary, model, provider,
run_target, node, agent, run_id, model_calls,
uncached_input_token_count, cache_read_input_token_count,
cache_creation_input_token_count, output_token_count,
reasoning_output_token_count, outcome
  • ts is the stored UTC time; ts_local is the same instant in tz.
  • A value the provider did not report is an empty cell, not 0.
  • The column order is fixed. New columns are only ever added at the end, so an import you build today keeps working after an upgrade.
  • UTF-8 with a BOM, so Excel shows non-English workflow and agent names correctly.
  • A text value that a spreadsheet would run as a formula (starting with =, +, - or @) is prefixed with '.
  • The X-Usage-Skipped-Lines header counts unreadable ledger lines; X-Usage-Coverage repeats the coverage note.
  • There are no price columns.

Reading the numbers

model_calls is usually larger than the number of turns. A turn that uses tools calls the model once per step, and each call is billed.

Input is split by price band, not merged. Cache reads and cache writes cost different amounts from ordinary input, and once added together they cannot be separated again -- so uncached_input_token_count, cache_read_input_token_count and cache_creation_input_token_count stay distinct. A high cache-read share is prompt caching working.

A field you do not see was not reported. If a provider reports no cache figures, the field is absent rather than 0, so you can tell "none used" from "not measured".

Reasoning is part of output. reasoning_output_token_count is already counted in output_token_count; do not add the two.

There are no prices here. Rates change independently of this system and depend on your contract; a stored price would go quietly stale. Multiply by your own rates.

Chats that leave no name

Temporary chats appear in day and month totals but group under (temporary) in the per-chat view: the conversation is designed to disappear, so there is no chat to attribute the spend to. The totals still add up.