meter.

collection → aggregation → rating → invoicing, with a reconciliation loop against the raw archive

1 · Collection — emit billable events

Events are validated, queued, archived to R2 and folded into their event-time bucket. Backdate one past the 72h lateness window, or emit into a closed period, to watch it route to adjustments instead.

2 · Aggregation — buckets by event hour

Derived, never incremented: each write recomputes SUM(quantity) over deduplicated events, so replay and reordering are no-ops. Polls every 2s.

meterevent hour (UTC)quantityevents
no buckets yet

3 · 4 · Rating and invoicing

Rated against the price book in KV, in integer cents, with the version stamped on every line. Closing is keyed on (account, period) and one-way.

meterquantityfreebillablerateamount
no usage in this period
Invoice preview$0.00

Adjustments — usage that could not move the number

Events that arrived after the period closed, or beyond the lateness window. Recorded and priced at the marginal rate, never folded into an issued invoice.

event_idmeterqtyevent timereason
none

Reconciliation — recompute from the R2 archive, diff against D1

Aggregates the raw archive independently and compares it to the ledger. Findings are keyed, so re-running updates them rather than duplicating them — and nothing here ever resolves itself. A period cannot close while an exception is open.

kindmeterevent hourarchiveD1detailstatus
no exceptions — the ledger matches the archive

Audit trail — append-only

Who closed a period, who resolved an exception. The actor is public-demo until PROJECT 3 puts Cloudflare Access in front of /admin/* and validates the JWT in the Worker.

#whenactoractiontargetdetail
nothing yet