> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voicegateway.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Immutable pricing and exact usage accounting

> Version 1 pricing revisions, decimal rating, durable usage receipts, ownership, and migration guidance

VoiceGateway's version 1 accounting ledger is an opt-in companion to the
legacy request-cost records. Legacy APIs remain available; new invoice-grade
integrations should use `/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 returns `409`. 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.

| Dimension                              | Fixed unit |
| -------------------------------------- | ---------- |
| text input/output and cache read/write | token      |
| realtime audio input/output/cache      | token      |
| characters                             | character  |
| audio                                  | second     |
| requests                               | request    |

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 the `admin` 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 migration `a6c9e2f4b817` 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:

1. Back up the SQL database and upgrade collectors.
2. Create and read back acquisition and selling revisions, verify their hashes,
   then activate each side independently.
3. Configure ownership, upgrade producers, and monitor unrated ledger rows plus
   each producer outbox's pending/rejected health side-by-side with legacy reporting.
4. Switch invoice exports only after reconciliation passes.

The existing `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

```python theme={null}
from voicegateway import attach
from voicegateway.accounting.outbox import AccountingOutbox

# Obtain one immutable preparation binding per offering before execution.
outbox = AccountingOutbox("accounting-outbox.db", "https://collector.example")
attach(
    session,
    accounting_outbox=outbox,
    accounting_binding={
        "provider/stt-model": prepared_stt_binding,
        "provider/llm-model": prepared_llm_binding,
        "provider/tts-model": prepared_tts_binding,
    },
    accounting_producer_id="agent-worker",
)
```

Supplying `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.
