Billing Wiring
Every billable event reaches record_operation() through a set of fire-and-forget helpers. This page covers the helpers, the call-site map, and the patterns that keep billing failures from breaking user-facing operations.
Fire-and-Forget Pattern
Section titled “Fire-and-Forget Pattern”All billing calls use tokio::spawn and never propagate errors. The chargeable event (call, message, AI generation) already happened — billing failures log at error! but don’t break the parent operation.
use crate::mods::billing::{OperationInput, OperationReference, spawn_record_operation};
spawn_record_operation( org_id, OperationInput::SmsOutbound { segments: 2 }, Some(OperationReference::Message(message_id)),);// Caller continues immediatelyFile: src/mods/billing/services/spawn_record_operation_service.rs
Helper Functions
Section titled “Helper Functions”spawn_record_operation()
Section titled “spawn_record_operation()”The core fire-and-forget wrapper. Spawns a tokio::task that acquires its own DB connection, calls record_operation(), and logs failures with structured fields (org_id, op_type, reference_type, reference_id).
pub fn spawn_record_operation( organization_id: Uuid, input: OperationInput, reference: Option<OperationReference>,)Typed Convenience Helpers
Section titled “Typed Convenience Helpers”These thin wrappers handle type-specific logic so call sites stay clean:
| Helper | What it does |
|---|---|
spawn_record_sms_outbound(org_id, body, message_id) | Calls estimate_sms_segments(body) internally |
spawn_record_sms_inbound(org_id, body, message_id) | Same segment estimation, required message ref |
spawn_record_email_outbound(org_id, message_id) | Fixed count: 1 per email |
spawn_record_realtime(org_id, provider, duration_secs, call_id) | Maps RealtimeProvider to the correct OperationInput variant |
ai_model_tier_for()
Section titled “ai_model_tier_for()”Maps model IDs (e.g. anthropic/claude-sonnet-4.5) to AiModelTier for credit pricing.
File: src/mods/billing/services/ai_model_tier_service.rs
pub fn ai_model_tier_for(model_id: &str) -> AiModelTierTier mapping (case-insensitive substring match):
| Tier | Models |
|---|---|
| Ultra | Claude Opus |
| Premium | Claude (Sonnet, Haiku, other variants) |
| Budget | DeepSeek, Gemini Flash |
| Mid | Gemini Pro, GPT-4/4o, unknown models (with warn! log) |
estimate_sms_segments()
Section titled “estimate_sms_segments()”Estimates Twilio segment count using GSM-7 / UCS-2 encoding rules. Always returns at least 1.
File: src/shared/utils/estimate_sms_segments.rs
pub fn estimate_sms_segments(body: &str) -> i32| Encoding | Single segment | Concatenated |
|---|---|---|
| GSM-7 (ASCII) | 160 chars | 153 per segment |
| UCS-2 (Unicode) | 70 chars | 67 per segment |
Note: Extended GSM-7 characters (
^,{,|,~,€) count as 2 chars in Twilio but 1 here. This can undercount by 1 segment in rare cases.
Call-Site Map
Section titled “Call-Site Map”AI Text and Transcription — Centralized
Section titled “AI Text and Transcription — Centralized”Both operation types are wired through spawn_log_ai_usage(), which already centralizes AI usage logging for 30+ generation sites.
File: src/mods/ai/services/log_ai_usage_service.rs
- AI Text: Sums
input_tokens + output_tokens + reasoning_tokens, callsai_model_tier_for()to determine tier, recordsOperationInput::AiText. Reference:AiUsageLog. - Transcription: Ceils
audio_duration_secstoi64, recordsOperationInput::Transcription. Reference:Callif available, otherwiseAiUsageLog.
Voice Recording
Section titled “Voice Recording”File: src/mods/twilio/utils/process_twilio_recording_util.rs
Fires after storing the recording to R2/disk, before transcription kicks off. Reference: Call.
Realtime Sessions (OpenAI / Gemini)
Section titled “Realtime Sessions (OpenAI / Gemini)”File: src/mods/twilio/routes/twilio_stream_route.rs
Fires at WebSocket disconnect (session finalization). Measures wall-clock duration via session_started_at.elapsed(). Skips if duration is 0. Reference: Call.
SMS Outbound (5 call sites)
Section titled “SMS Outbound (5 call sites)”| Call site | File |
|---|---|
send_message_api (non-attachment) | src/mods/messaging/api/send_message_api.rs |
Plan executor (execute_send_sms) | src/mods/plan/services/execute_send_sms_service.rs |
| Text agent auto-reply | src/mods/text_agent/services/handle_inbound_message_for_text_agent_service.rs |
Assistant send_sms tool | src/mods/assistant/tools/messages/ai_send_sms_tool.rs |
Assistant send_bulk_sms tool | src/mods/assistant/tools/messages/ai_send_bulk_sms_tool.rs |
Bulk SMS: Batches all segments into one OperationInput::SmsOutbound with segments = segments_per_msg × sent_count. No per-message reference.
SMS Inbound
Section titled “SMS Inbound”File: src/mods/twilio/api/twilio_events_api.rs
Fires after creating the inbound message record, before processing media attachments.
MMS Outbound
Section titled “MMS Outbound”File: src/mods/messaging/api/send_message_api.rs
Triggered when channel == Sms AND the message has attachments. Records OperationInput::MmsOutbound { count: 1 }.
Email Outbound (2 call sites)
Section titled “Email Outbound (2 call sites)”| Call site | File |
|---|---|
send_message_api (email path) | src/mods/messaging/api/send_message_api.rs |
Plan executor (execute_send_email) | src/mods/plan/services/execute_send_email_service.rs |
Related Pages
Section titled “Related Pages”- Operation Metering —
record_operation(), credit waterfall, and operation types - Cycle Usage View — the UI that reads the data written by these call sites
- Billing Overview — architecture and credit model