Operation Metering
Every billable event in Loquent — a voice call, an SMS, an AI generation — flows through record_operation(). This service converts raw measurements into billed units, translates those units into credits via billing_config rates, and runs a credits-only waterfall against org_budget.
Operation Types
Section titled “Operation Types”The OperationType enum defines all billable event categories:
pub enum OperationType { VoiceCall, // Billed in ceil-minutes VoiceRecording, // Billed in ceil-minutes SmsOutbound, // Billed per segment SmsInbound, // Billed per segment MmsOutbound, // Billed per message AiTextBudget, // Billed per 1K tokens (DeepSeek, Gemini Flash Lite) AiTextMid, // Billed per 1K tokens (Gemini Pro, GPT-4.1) AiTextPremium, // Billed per 1K tokens (Claude Sonnet) AiTextUltra, // Billed per 1K tokens (Claude Opus) Transcription, // Billed in ceil-minutes RealtimeOpenai, // Billed in ceil-minutes RealtimeGemini, // Billed in ceil-minutes EmailOutbound, // Billed per email}AI text operations map from AiModelTier (Budget, Mid, Premium, Ultra) via OperationType::from_ai_model_tier().
Recording an Operation
Section titled “Recording an Operation”Callers construct an OperationInput and pass it to record_operation():
use crate::mods::billing::{OperationInput, OperationReference, record_operation};
let receipt = record_operation( &db, organization_id, OperationInput::VoiceCall { duration_secs: 187 }, Some(OperationReference::Call(call_id)),).await?;OperationInput Variants
Section titled “OperationInput Variants”Each variant carries the operation’s native measurement:
| Variant | Field | Unit |
|---|---|---|
VoiceCall | duration_secs: i64 | Seconds → ceil-minutes |
VoiceRecording | duration_secs: i64 | Seconds → ceil-minutes |
SmsOutbound | segments: i32 | Segments |
SmsInbound | segments: i32 | Segments |
MmsOutbound | count: i32 | Messages |
AiText | tokens: i64, model_tier: AiModelTier | Tokens → ceil-1K |
Transcription | duration_secs: i64 | Seconds → ceil-minutes |
RealtimeOpenai | duration_secs: i64 | Seconds → ceil-minutes |
RealtimeGemini | duration_secs: i64 | Seconds → ceil-minutes |
EmailOutbound | count: i32 | Emails |
Credits-Only Waterfall
Section titled “Credits-Only Waterfall”record_operation() converts native units to credits once before the waterfall starts, then deducts in a single unit (credits) across four pools:
native units → credits_required = ceil(units × billing_config.*_credits rate) │ ▼ ┌─────────────────────┐ │ 1. Op-type pool │ org_budget.*_included (per-operation credits) └────────┬────────────┘ │ remainder ▼ ┌─────────────────────┐ │ 2. Included credits │ org_budget.included_credits (general pool) └────────┬────────────┘ │ remainder ▼ ┌─────────────────────┐ │ 3. Purchased credits│ org_budget.purchased_credits (top-ups) └────────┬────────────┘ │ shortfall ▼ ┌─────────────────────┐ │ 4. Overdraft │ Drives included_credits negative └────────┬────────────┘ │ remaining shortfall ▼ OverdraftLimitExceededKey details
Section titled “Key details”- Tier gate: if
subscription_tier.*_includedisNULLfor this operation type, the operation is rejected withOperationNotAllowed— regardless of credit balance. - Op-type pool:
org_budget.*_includedstores credits (not native units). Cycle reset refills this fromsubscription_tier.*_included. - Defensive floors:
op_pool_remainingandpurchased_creditsare floored at 0 before arithmetic to prevent negative DB values from inflating downstream deductions. - Overdraft: shortfall beyond purchased credits drives
included_creditsnegative. Runaway orgs (no limit) absorb all shortfall; bounded orgs block at-overdraft_limit. - Concurrency:
SELECT ... FOR UPDATEonorg_budgetserializes concurrent ops per org.
Credit conversion
Section titled “Credit conversion”The billing_config table holds one rate per operation type (e.g. voice_call_credits = 15). Conversion happens in credits_for():
fn credits_for(units: Decimal, rate: i32) -> Result<i32, AppError> { let credits = units * Decimal::from(rate); credits.ceil().to_i32() .ok_or_else(|| AppError::Internal("credit calculation overflow".into()))}A 4-minute voice call at rate 15 costs 4 × 15 = 60 credits.
Tier Allowances (Credits)
Section titled “Tier Allowances (Credits)”Both subscription_tier.*_included and org_budget.*_included store credits, not native units. The migration m20260422_120000 multiplied each existing native-unit value by its billing_config.*_credits rate:
-- Example: voice_call_included was 600 minutes, rate is 15 credits/min-- After migration: voice_call_included = 9000 creditsUPDATE subscription_tier SET voice_call_included = voice_call_included * (SELECT voice_call_credits FROM billing_config LIMIT 1), -- ... 12 more columnsNULL (operation not available on tier) and 0 are preserved.
OperationReceipt
Section titled “OperationReceipt”record_operation() returns a receipt showing the credit breakdown:
pub struct OperationReceipt { pub operation_id: Uuid, pub operation_type: OperationType, pub units: Decimal, // Native units (for audit) pub free_units_used: Decimal, // Credits from op-type pool (name kept for schema stability) pub included_credits: i32, // Credits from included pool pub purchased_credits: i32, // Credits from purchased pool pub overdraft_credits: i32, // Portion that drove included_credits negative}free_units_used stores credits (not native units) despite its column name — the DB schema retains the original name for stability.
Pre-flight Check
Section titled “Pre-flight Check”Use check_operation_allowed() for a lightweight check before starting expensive operations. It verifies subscription status and tier gates without locking org_budget.
Operation References
Section titled “Operation References”Each operation links back to its source entity:
pub enum OperationReference { Call(Uuid), Message(Uuid), AiUsageLog(Uuid),}Stored as (reference_type, reference_id) on the operation table.
Pre-Launch Escape Hatch
Section titled “Pre-Launch Escape Hatch”Orgs without an org_budget row (not yet onboarded) get a zero-cost receipt and a warn! log. This escape is removed once all orgs are backfilled with subscriptions.