Skip to content

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.

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 immediately

File: src/mods/billing/services/spawn_record_operation_service.rs

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>,
)

These thin wrappers handle type-specific logic so call sites stay clean:

HelperWhat 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

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) -> AiModelTier

Tier mapping (case-insensitive substring match):

TierModels
UltraClaude Opus
PremiumClaude (Sonnet, Haiku, other variants)
BudgetDeepSeek, Gemini Flash
MidGemini Pro, GPT-4/4o, unknown models (with warn! log)

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
EncodingSingle segmentConcatenated
GSM-7 (ASCII)160 chars153 per segment
UCS-2 (Unicode)70 chars67 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.

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, calls ai_model_tier_for() to determine tier, records OperationInput::AiText. Reference: AiUsageLog.
  • Transcription: Ceils audio_duration_secs to i64, records OperationInput::Transcription. Reference: Call if available, otherwise AiUsageLog.

File: src/mods/twilio/utils/process_twilio_recording_util.rs

Fires after storing the recording to R2/disk, before transcription kicks off. Reference: Call.

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.

Call siteFile
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-replysrc/mods/text_agent/services/handle_inbound_message_for_text_agent_service.rs
Assistant send_sms toolsrc/mods/assistant/tools/messages/ai_send_sms_tool.rs
Assistant send_bulk_sms toolsrc/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.

File: src/mods/twilio/api/twilio_events_api.rs

Fires after creating the inbound message record, before processing media attachments.

File: src/mods/messaging/api/send_message_api.rs

Triggered when channel == Sms AND the message has attachments. Records OperationInput::MmsOutbound { count: 1 }.

Call siteFile
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