| A re-report overwrites; it never double-countsC104 | the record is an upsert on (workspace, build session, transcript id) behind the unique index UQ_build_session_usages_report; the row is read inside the transaction under UPDLOCK, HOLDLOCK — a key-range lock that covers the key before the row exists, so two concurrent first reports for one transcript serialise instead of racing to a second insert | shipped |
| A stale report is discarded, not appliedC104 | window_end is the report-ordering token: when the incoming report and the stored row both carry one and the incoming one is older, nothing is written and the response carries applied false with the row as it stands. Serialising two reports decides which lands last, not which is newer — so this is the half a lock could never buy | shipped |
| A first report is 201 Created, an overwrite is 200 OKC104 | the service returns inserted or overwritten and the endpoint maps that to the status; the body carries the same created flag on both transports, so a client can tell which happened without guessing | shipped |
| The row is mutable on purpose, and its first-seen time is notC104 | this is the one session-state table with no trigger: an overwrite replaces the counts, the window and the models, preserves created_at and bumps updated_at — clamped so it can never fall before created_at when a reporting clock is skewed | shipped |
| The counts are the client’s, and PostMQ measures none of themC110self-reported: the numbers are as good as the client that sends them; a client that reports nothing produces no row and no gap you can see | every token, turn, model and window value in the row is copied from the request; the server validates bounds and stores it. PostMQ does not connect to your model provider, does not observe your agent, and holds no price table | shipped with caveat |
| The five token classes stay apartC105 | input, cache write (5m), cache write (1h), cache read and output are separate BIGINT columns with a non-negative CHECK, never one total — they are billed at different rates and cache read dominates agentic coding, so a single number would hide the shape | shipped |
| Turns are bounded by their own arithmeticC105 | turns and sidechain_turns are non-negative and sidechain_turns may not exceed turns; window_end may not precede window_start; each rule is both a service check (a 400) and a database CHECK constraint | shipped |
| The five rollup axes, one queryC106the period is computed from created_at — when the report landed — not from the window the report describes | total, project, day, week and month; total and project aggregate in SQL, day, week and month project the rows and bucket in memory; week is Monday-anchored and month starts on the first, both keyed yyyy-MM-dd | shipped with caveat |
| Per-project cost follows the session, not a copy of itC106 | the rollup joins build_sessions for each session’s current project rather than denormalising a project onto the usage row, so re-homing a session moves its historical usage with it; the project rows come back biggest first, by cache read plus input plus output | shipped |
| Writes need write_session_state; reads need only a signed-in callerC107 | POST /v1/usage carries RequireScope("write_session_state") and the record tool passes the same scope constant to its gate; GET /v1/usage carries no scope and accepts a bearer of either tier or a dashboard session, and the read tool only authenticates — the same rule over REST and MCP | shipped |
| The workspace comes from the credentialC107 | on every path the workspace is read from the resolved bearer or session and never from the arguments or the body; an unknown build session in that workspace is a not-found, not a foreign-key error | shipped |
| A usage report writes no audit rowC108not audited, on purpose — every session-state lifecycle event is still on the workspace’s tamper-evident chain; this high-frequency touch is not one | there is no usage event family in the audit event types at all, and the service names no audit writer; the write path takes only the workspace from the credential because the row records no actor. It is the same deliberate exception as a session ping | shipped with caveat |
| REST and MCP return the same bytesC05 | both surfaces build the record result and the rollup through one shared session-state serializer, so the two transports are byte-identical by construction and tested that way | shipped |
| People see totals and a breakdownC109there is no chart on this view today, no export and no dollar figure; the flow charts are a separate view | app.postmq.com/usage — a project filter, a break-down-by picker, a from and a to date; a Totals card of eight figures and a breakdown table of nine columns, thousands-separated | shipped with caveat |
| Nothing acts on the numberC110awareness, not enforcement — if you need a ceiling, this is not it today | no sweep, gate, limiter or plan check reads the usage table — only the rollup does. There is no budget, no threshold, no alert and no enforcement: usage is never consulted to stop a send, a session or a credential | shipped with caveat |