Skip to main content

Wallet (Telephony Balance)

Audience: Engineering · Where in app: sidebar wallet pill; Packages → Wallet · Since: 2026-07

The "wallet" is not a new balance — it reuses SubscriptionModel.remaining_purchased_ai_credits (purchased AI-credit add-ons, 1 credit = $0.01). It funds telephony (number purchase, monthly rental, per-call Twilio cost) and doubles as the overflow pool for AI usage.

Semantics

BalancePurposeCycle behavior
remaining_ai_creditsPlan credits — AI-only, never touched by telephonyReset to the plan allotment on renewal
remaining_purchased_ai_creditsThe wallet — telephony and AI fallbackCarries over — renewal jobs reset plan credits only
  • AI draw-down (AITokensCreditJob) spends plan credits first, then falls into the wallet.
  • Telephony debits come only from the wallet. The balance may go negative (burst / mid-call) — carried forward and paid down by the next top-up.

Gates

GET /internal/telephony/wallet/:company_id returns both flags:

  • has_telephony: wallet > 0 — premium gate for buy/rental/calls.
  • ai_available: plan > 0 || wallet > 0 — AI falls back to the wallet.
  • Plus markup_percent (see below) so ms-ai gates number purchase on the marked-up price and the frontend shows final prices.

Backend endpoints

backend/app/controllers/TelephonyWalletController.js, routes in backend/app/routes/internalTelephonyRoutes.js, ms-ai → backend auth via apiKeyAuth (BACKEND_API_KEY):

RouteWhat
GET /internal/telephony/wallet/:company_idBalance + gate flags + markup_percent
POST /internal/telephony/debitIdempotent debit: reserves idempotency_key (unique index on ai_credit_logs) before the $inc, so the 10-min call sweep / rental cron never double-charge
POST /internal/telephony/notifyRental suspend_warning (day 0/3/6) / released emails — backend owns templates + billing-user resolution

Markup. credits in the debit body is the raw Twilio cost; the wallet is charged base × (1 + markup% / 100) with sub-cent precision (deliberately not rounded to a whole cent). Markup comes from the settings key telephony_markup_percent (default 0); margin_credits is recorded on the ledger row for revenue reporting.

Billing-user resolution (_resolveBillingUser): sub-account owner if they have their own card → agency owner → subscription owner. Notify emails are best-effort — a mail failure never fails the request.

Crons

CronWhere / scheduleWhat
number-rental:meter-duems-ai src/elevenlabs/number-rental.scheduler.ts, daily 06:00 UTCMonthly number rental; idempotent on (number, period), catches up missed days
twilio-call-cost:meterms-ai, every 10 minSweeps each company subaccount's completed calls; idempotent on CallSid
number-rental:suspension-sweepms-ai, every 15 minSuspend → warn (day 0/3/6 via /notify) → release after 7 days
BalanceAutoRechargeJobbackend scheduler, every minuteTops up the wallet from the saved card when below balance_auto_recharge.threshold_credits

Kill switch for all ms-ai rental crons: NUMBER_RENTAL_CRON_ENABLED=false. Bring-your-own Twilio numbers are never metered. The suspension sweep intentionally runs slower than auto-recharge so a healthy card tops up before anything suspends.

Telephony charges are recorded in ms-ai cost-tracking via recordFlatUsdCharge (same mechanism as Agent K provider usage, separate providers for reconciliation) — see cost-tracking.

Auto-recharge

SubscriptionModel.balance_auto_recharge subdoc: { enabled, prompted, threshold_credits, addon_id }, managed via SubscriptionController (enabling requires an addon selected). Purchases with no card on file return a clear 402 (payment error, not a 500).

Frontend

  • Sidebar pillsrc/components/company/CompanyNavigationSidebar.vue (walletUsd, amber when low).
  • Wallet pagesrc/views/packages/Wallet.vue, GET /subscription/wallet-transactions. Type badges: buy_addon (Top-up), balance_auto_recharge (Auto-recharge), telephony_rental (Number rental), telephony_call (Call), telephony_debit (Telephony).
  • Tabssrc/components/packages/PackagesTabs.vue: Wallet / Add-ons / Payment Methods.
  • Accesssrc/composables/useBalanceAccess.js now grants canUseTelephony and canSelfServeTopup to everyone, including agency sub-accounts (full self-service; the old agency-funded restriction is lifted). isAgencySubAccount remains only for copy/tab tailoring — subscription & invoices stay agency-owned.