/v1/accounting/*.
Guarantees
Pricing has independent acquisition and selling revisions. A revision is identified by tenant scope, side, revision ID, contract version, and the SHA-256 hash of canonical JSON. Repeating identical content is idempotent. Reusing an identity with different content returns409. Activation is a
separate operation and can include expected_current_revision_id to prevent a
lost update. Failed creation or activation does not change the previous active
revision.
POST /v1/accounting/prepare captures active revisions and accounting
ownership for one project, component, and offering. The resulting binding is
immutable. Provider attempts retain that binding across delayed delivery and
collector retries. A later price activation affects only later preparations.
Missing revisions produce an unrated event; VoiceGateway never applies the
current catalog retroactively.
Usage commits transactionally with a per-record receipt. The SQL ledger
(SQLite or PostgreSQL) is authoritative. The reserved projection table is not
written and pending_delivery is not reported until a real consumer ships;
ClickHouse accounting projection remains deferred. Duplicate event payloads
return their original receipt, while conflicting content or a second billable
producer for the same provider attempt is durably rejected.
Exact units and rounding
Rates and quantities are JSON decimal strings. Binary floating-point is not used by the version 1 rater.
Cache quantities are subsets of text input. The rater subtracts cache read and
write from ordinary input only when that cache dimension has its own rate. An
unpriced cache subset stays in ordinary input and marks the side incomplete
instead of silently discounting tokens. A revision must
classify every dimension as rated or unsupported. Unknown prices, missing
measurements, unsupported measurements, and explicit zero prices are distinct.
The
usd-v1-half-even-12 profile computes with 60 decimal digits and rounds
each event-side total to 12 USD fractional digits using half-even rounding.
Stored event totals are aggregated; display rounding is not accounting.
Version 1 supports USD only and rejects other currencies.
Security and data minimization
The authenticated principal supplies the tenant. A submitted tenant identifier is never trusted as authority. Project-restricted API keys may ingest and read only their allowed projects. Acquisition rates, acquisition totals, and margin are operator-only; tenant reports, dashboard responses, and the read-only MCP status tool expose selling totals and completeness only. The usage schema is an allowlist. It has no fields for credentials, prompts, transcripts, caller values, arbitrary metadata, or tool arguments. Do not add these fields to accounting extensions. Pricing mutations require both an admin principal and theadmin scope. A
tenant-bound admin remains bound to that tenant. Producer-facing preparation
responses omit acquisition revision IDs; during ingestion, the collector
resolves acquisition, selling, and ownership from the stored binding.
Producer delivery
AccountingOutbox.submit() is the durable producer API. It writes the exact
envelope and stable event ID to a record- and byte-bounded SQLite outbox before
returning.
attach() registers its LiveKit component and session event handlers
synchronously before it returns. When a native metric arrives, its sink work is
scheduled outside provider execution and the accounting decorator calls
submit() before writing ordinary telemetry. Collector I/O happens only during
drain, so a collector outage cannot delay the voice response. A full disk
outbox or persistence failure increments an explicit capture-failure signal,
persists that counter when storage is available, and logs an error.
enqueue_nowait() remains available for producers that cannot await even a
local write. It has a documented process-crash window before its background
worker persists the event. attach() does not use that weaker path.
The outbox never evicts persisted, unacknowledged usage. It deletes a row only
after an accepted or duplicate receipt with a receipt ID, quarantines
terminal rejections and HTTP 4xx responses, and retains retryable or
missing-receipt rows across restarts. Backoff is bounded and jittered; rows that
exhaust the configured attempt limit are quarantined.
Operators must monitor pending count, oldest pending age, rejected count, and
capture failures. Bounded storage and an indefinitely unavailable collector
cannot mathematically guarantee zero loss; this condition is visible rather
than silent.
Delivery retries preserve event and attempt IDs. A new provider retry or
fallback receives a new attempt and event ID. SDK-owned and externally owned
accounting are selected in an operator-managed ownership assignment. Diagnostic
telemetry may still be emitted by the non-owner, but it must not be submitted
as billable usage.
Native LiveKit request IDs and streaming segment IDs are retained as
non-content correlation metadata. They generate replay-stable accounting event
IDs, so replaying one metric deduplicates while different TTS segments that
share a request ID remain distinct. Realtime text, audio, and cached-audio token
quantities are carried separately. An absent native measurement is emitted as
missing, never as a measured zero.
The remaining observation boundary is explicit: work completed before
attach() returns, a provider that emits neither a metric nor cumulative
session usage, and an abrupt process termination before the scheduled metric
task starts cannot be reconstructed. Graceful session close reconciles missing
per-call metrics from cumulative LiveKit usage. The Wave 0 tracing context does
not create accounting records; enabling or propagating that context leaves the
single accounting capture sink as the only billable producer.
Compatibility and migration
Database migrationa6c9e2f4b817 adds the ledger and optional project allowlists
to API keys. Existing keys remain unrestricted within their tenant. Existing
request rows keep their stored floating-point totals and are labeled by their
legacy provenance; they are not re-rated or assigned invented revisions.
Rollout order:
- Back up the SQL database and upgrade collectors.
- Create and read back acquisition and selling revisions, verify their hashes, then activate each side independently.
- Configure ownership, upgrade producers, and monitor unrated ledger rows plus each producer outbox’s pending/rejected health side-by-side with legacy reporting.
- Switch invoice exports only after reconciliation passes.
voicegw export-costs command exports legacy request-cost rows.
It is not an immutable-ledger export and contains no acquisition revision,
acquisition total, or margin fields. A versioned exact-accounting export is
deferred; use the tenant-scoped report API for supported ledger reads.
To roll back, stop new version 1 producers, drain or preserve their local
outboxes, and deploy the earlier application. Do not downgrade the database:
the additive tables are inert to older code and retain receipts for a later
resume. Migration a6c9e2f4b817 explicitly refuses destructive downgrade so a
routine rollback cannot erase ledger data or project allowlists.
Minimal SDK example
accounting_outbox is the explicit opt-in. Normal telemetry continues
through its configured sink, while usage-v1 capture persists without blocking
the voice response. If prepared ownership is external, the SDK does not send
a duplicate billable record.
The read-only MCP accounting tool accepts no tenant argument. Bind it to one
tenant with VOICEGW_MCP_ACCOUNTING_TENANT; without that setting it returns
accounting_tenant_not_configured and no accounting data.
Provider adapters must report unsupported measurements explicitly. They must
not infer absent cache, realtime-audio, reasoning, or character measurements.
For a complete synchronization and delayed-delivery example, run
python examples/accounting_sync.py. It discovers capabilities, synchronizes
acquisition and selling independently, verifies readback hashes and dimension
sets, prepares a binding, changes the active selling revision, and submits a
delayed event against the pinned older revision with a per-record receipt.