Once metering has landed usage events in Postgres, control exposes them as a read-only projection. One endpoint answers “what did this workspace consume, in this window, broken down this way” — and tells you how fresh and how complete that answer is.

The endpoint

Usage summary
Implemented by 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 aggregates usage_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
The response is validated against packages/proto/operator_usage_summary.schema.json before it is returned. Totals and every group carry the same fields:
usd_micros is an estimate of your vendor cost, computed from the rates in your own workspace pricebook. It is not Obol-invoiced spend, and a workspace with an empty pricebook reports a zero subtotal with a non-zero unpriced_count. What Obol charges you is a separate ledger: enrolled design partners invoice on the grandfathered 1/1 / 3 / 10/10 / 1 per 1,000 meters, distinct from the public 0.30/0.30 / 1.00 / 3.00/3.00 / 0.10 ladder — see Stripe Billing.
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.
Because budget posture comes from Redis and usage totals come from Postgres, the two can disagree briefly. That is the intended shape of ADR-0006: spend counters are authoritative for enforcement in the moment, and the SQL projection is the durable record that catches up.

The pricebook

The rates behind usd_micros are per-workspace and versioned. apps/control/app/api/pricing.py exposes them:
Pricebook
Reading requires 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.

Receipts

Per-tool-call records, queried separately from usage aggregates.

Audit log

Who changed the workspace, including who replaced the pricebook.