The endpoint
Usage summary
apps/control/app/services/usage_query.py and served from apps/control/app/api/observe.py. See the Observe API reference.
Access requires workspace membership plus the
usage.read action, which every role from viewer upward holds. A malformed window returns 422; a database or schema-validation failure returns 503 with Observe read model unavailable, never a partial body.
There is no raw per-event feed. The projection is aggregate-only. These attempt-level operational counts differ from the deduplicated logical calls shown in Settings → Billing.
Aggregation windows
Windows are caller-supplied. There are no fixed hourly or daily buckets, no server-side rollup tables, and no retention-driven downsampling — every request aggregatesusage_events rows over exactly the range you name. To chart a series, issue one request per bucket.
Grouping is a single dimension. group_by=key returns one row per virtual key; group_by=tool returns one row per namespaced tool name. Groups are ordered with NULL last (a model name is absent on a tool event, and a tool name is absent on a model event), and the null group is a legitimate result rather than an error.
What comes back
Response shape
packages/proto/operator_usage_summary.schema.json before it is returned. Totals and every group carry the same fields:
Integer aggregates are preserved exactly across the JSON boundary: PostgreSQL returns
SUM(bigint) as a decimal, and the projection converts it with int() rather than a float or a narrowing cast.
Freshness and completeness
freshness_at is the maximum ingested_at in the window — how recently the drain wrote anything relevant. A null means nothing in that window has been ingested.
completeness is either complete or gap_detected. gap_detected means the workspace carries a sticky usage_ingestion_gap_at marker: the bounded usage transport lost entries, so these totals are a floor, not a total. That marker also pauses Stripe meter delivery for the workspace, and nothing clears it automatically (ADR-0045).
Budget posture
budgets is a separate read, and deliberately not a SQL aggregate. Budgets are enforced by the gateway against Redis counters, so the honest way to report remaining budget is to read the same counters the gateway reads.
For each active, unrevoked, unexpired virtual key in the workspace, the projection reads spend:{key_id}:{yyyyMM} for the current calendar month and reports:
Any Redis failure — unreachable, or a counter value that is not a plain decimal integer — projects
state: "unavailable" with null figures for every key. It never projects zero. An unreadable budget must not look like an unused one.The pricebook
The rates behindusd_micros are per-workspace and versioned. apps/control/app/api/pricing.py exposes them:
Pricebook
usage.read. Replacing requires pricing.write (admin and owner), passes an expected_revision for optimistic concurrency, writes an audit row, and publishes a new workspace snapshot so the gateway prices subsequent calls with the new rates. Rates are integer micro-USD and must be non-negative; an explicit zero is a supported estimate, while an absent rate stays unknown.
Related surfaces
Receipts
Per-tool-call records, queried separately from usage aggregates.
Audit log
Who changed the workspace, including who replaced the pricebook.