# Media & non-text billing — usage-basis taxonomy

Reference for how matrx-ai turns a model's `ai_model.pricing` row into a dollar
cost for **image / video / audio / TTS / STT / realtime** generation. Text LLM
billing is the trivial case (real tokens × $/1M); everything else rides the same
formula via a **usage basis**.

> **The one rule:** cost is always `billing_tokens / 1_000_000 × price`. A
> media/audio model bills correctly *only* when its tier's `usage_basis` matches
> how the provider reports usage **and** the billable unit count is populated at
> the call site. Get either wrong and cost silently collapses to $0 or explodes
> 1,000,000×. Every bug in the 2026-06-19 sweep was one of those two.

## SSOT

`usage_config.USAGE_BASIS_SPECS` is the single source of truth. Each basis fixes:
the billed token field, how many synthetic "tokens" one real unit is worth
(`tokens_per_unit`), whether the unit must be **computed** from the media, and
the human price label. The matrx-frontend admin pricing editor
(`features/ai-models/usageBasis.ts`) mirrors it — keep them in sync.

| `usage_basis` | billed field | 1 unit = | price means | computed? |
|---|---|---|---|---|
| `null` (token) | input+output+cached | 1 token | $/1M tokens | no |
| `image_output` | output | 1,000,000 tok | $/image | no |
| `megapixel_output` | output | 1,000,000 tok | $/megapixel | **yes** (w×h) |
| `video_unit_output` | output | 1,000,000 tok | $/clip | no |
| `video_second_output` | output | 1,000,000 tok | $/second | **yes** (duration) |
| `minute` | input | 1,000,000 tok | $/minute | **yes** (session duration) |
| `character_input` | input | 1 tok | $/1M characters | no |
| `audio_second_input` | input | 100 tok (sec×100) | $/1M of those units | no |
| `audio_hour_input` | input | 1,000,000 tok | $/audio hour | **yes** (duration) |

## How cost is produced

- **Image / video** (`BaseMediaGeneration` subclasses): `_build_usage` resolves
  the basis and bills accordingly — per-asset for `image_output`/`video_unit_output`,
  **real pixels** for `megapixel_output`, **real seconds** for `video_second_output`.
  A token-priced model with `usage_basis=null` (gpt-image-\*, Gemini image-native)
  must override `_provider_usage(raw)` to surface the provider's real token usage —
  the synthetic per-asset count must NEVER hit a token-priced tier.
- **TTS** (raw-bytes endpoints): providers call
  `build_character_billed_usage_async(characters=…)` — bills `character_input`
  models by the post-dictionary character count.
- **Transcription** (Groq Whisper): `processing/audio` populates
  `input_tokens = floor(seconds×100)` for `audio_second_input`.

## Guardrails (defense in depth)

1. **`calculate_cost` warning** — if a tier has a basis but the billable field is
   0, or the basis is unknown, it screams once (deduped) with the exact fix.
2. **`validate_model_pricing`** — pure validator catching missing basis on a media
   model, `$/char`-as-`$/1M-char` (1e6×), unknown basis, zero-billed field.
3. **`scripts/validate_model_pricing.py`** — loud, non-blocking drift guard over
   every model (`--strict` for a CI gate). Also wired into `AiModelValidator`.
4. **Admin pricing editor** — basis dropdown + per-unit labels + inline validation
   so wrong entry is structurally hard.

## Adding a media model — checklist

1. Pick the `usage_basis` that matches the provider's billing.
2. If the provider returns **real token usage** and you want token billing, leave
   `usage_basis=null` and override `_provider_usage` on the provider class; add the
   `api_class` to `_TOKEN_BILLED_MEDIA_API_CLASSES` in `usage_config.py`.
3. Otherwise set the synthetic basis + the per-unit price.
4. Run `uv run python scripts/validate_model_pricing.py` — it must be clean.

---

## Open items (deferred — see FOUND_DEFECTS.md + feedback 49068dac / d4c9b77b)

These are **unwired metering paths**, not active mis-charges (the models bill $0
today; some may not be in production). The taxonomy + validator make wiring trivial.

### 1. Realtime voice per-minute (`realtime-api`, basis `minute`, $0.05/min)
xAI realtime voice is browser-orchestrated; `aidream/services/ai_execution/realtime_tools.py`
only resolves tools — no server-side session duration exists. **Blueprint:** the
browser client reports session length at end →
`POST /ai/realtime/session-usage {request_id, minutes}` (authed, idempotent on
request_id) → record a TokenUsage with `input_tokens = round(minutes × 1_000_000)`,
`api="xai"`, `matrx_model_name="realtime-api"` → `calculate_cost` does the rest.
Design care needed: reconnects, abandoned sessions, double-report guard.

### 2. ElevenLabs / xAI STT billing path (`scribe_v2*` `audio_hour_input`; `xai_stt`)
Only Groq Whisper transcription is wired today. When ElevenLabs/xAI STT is invoked,
populate the billable unit from the probed audio duration —
`input_tokens = round(hours × 1_000_000)` (`audio_hour_input`) or `floor(sec×100)`
(`audio_second_input`) — mirroring `processing/audio/audio_preprocessing.py`.

### 3. `xai_stt` (`speech-to-text`) has `pricing=null`
No price at all, and unwired. Decision for the owner: add its real rate + basis,
or mark `is_deprecated` if it's a placeholder. The drift guard nags until resolved.

### 4. gpt-image text-vs-image input rate (deliberate non-fix)
OpenAI gpt-image-\* charge text input ($5/1M) and image input ($8/1M) at different
rates; `PricingTier` has a single `input_price` (set to the image rate, 8). Effect:
text-to-image input is overcharged by ~$0.002/image; **image edits are already
correct** (image-rate input). Fixing needs a 4-rate schema extension (text/image ×
normal/cached) across server billing, validator, FE, and a DB backfill — ROI doesn't
justify it for sub-cent accuracy. Revisit only if edit volume / text-input share grows.
