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), Agent K sell-side provider charges (per-step debits at call time incl. the LinkedIn/Unipile monthly seat renewal — see Agent K), and doubles as the overflow pool for AI usage.
Semantics
| Balance | Purpose | Cycle behavior |
|---|---|---|
remaining_ai_credits | Plan credits — AI-only, never touched by telephony | Reset to the plan allotment on renewal |
remaining_purchased_ai_credits | The wallet — telephony and AI fallback | Carries over — renewal jobs reset plan credits only |
- AI draw-down (
AITokensCreditJob) spends plan credits first, then falls into the wallet. - Telephony and Agent K debits come only from the wallet. The balance may go negative (burst / mid-call) — carried forward and paid down by the next top-up; the UI renders negative balances in red rather than clamping.
- Agent K debits go through
AgentKWalletService+lib/agentKPricing.js(super-admin sell prices, settings rowagent_k_provider_pricing; idempotentai_credit_logsrows; unpriced steps free; failed provider calls unbilled).
Low-credit emails
Reworked in AITokensCreditJob:
- "Running low" (
ai-credits-low.html) fires once per allocation at theLOW_CREDITS_WARN_ATthreshold, guarded bySubscriptionModel.ai_credits_low_notified_at(stamped only after the mail is queued; cleared on a new grant). Suppressed when auto-recharge is on — a recharge would re-arm the warning and turn it into a recurring mail. - "Depleted" is deliberately NOT suppressed by auto-recharge: hitting zero with it enabled means the recharge itself failed (declined/missing card).
- Only one of the two sends in a given pass, never both.
AI-scrape gating
backend/app/lib/aiCredits.js — the single combined-balance rule (remaining_ai_credits + remaining_purchased_ai_credits > 0, active subscription required). Used by AutoScrapeJob (skip scheduled re-crawls for unfunded companies) and CompanyWebsiteController.scrape (reject manual "Add Website" scrapes with NO_CREDITS_MESSAGE); mirrors the balance test CompanyController applies for the UI so the scraper and the UI give the same verdict.
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):
| Route | What |
|---|---|
GET /internal/telephony/wallet/:company_id | Balance + gate flags + markup_percent |
POST /internal/telephony/debit | Idempotent 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/notify | Rental 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
| Cron | Where / schedule | What |
|---|---|---|
number-rental:meter-due | ms-ai src/elevenlabs/number-rental.scheduler.ts, daily 06:00 UTC | Monthly number rental; idempotent on (number, period), catches up missed days |
twilio-call-cost:meter | ms-ai, every 10 min | Sweeps each company subaccount's completed calls; idempotent on CallSid |
number-rental:suspension-sweep | ms-ai, every 15 min | Suspend → warn (day 0/3/6 via /notify) → release after 7 days |
BalanceAutoRechargeJob | backend scheduler, every minute | Tops 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 pill —
src/components/company/CompanyNavigationSidebar.vue(walletUsd, amber when low). - Wallet page —
src/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), plus Agent K line items:agent_k_apollo_search(lead search),agent_k_apollo_match(contact reveal),agent_k_predictleads(buying signals),agent_k_apify_sos(new businesses),agent_k_unipile_connect(LinkedIn connected),agent_k_unipile_renewal(LinkedIn monthly). Negative balances and debit rows render red. - Tabs —
src/components/packages/PackagesTabs.vue: Wallet / Add-ons / Payment Methods. - Access —
src/composables/useBalanceAccess.jsnow grantscanUseTelephonyandcanSelfServeTopupto everyone, including agency sub-accounts (full self-service; the old agency-funded restriction is lifted).isAgencySubAccountremains only for copy/tab tailoring — subscription & invoices stay agency-owned.
Related
- AI Credits — draw-down order, audit log
- Plans & Subscriptions
- AI Cost-Tracking (Admin) — where telephony/provider charges land