Credit Auto-Recharge
Auto-recharge monitors your org’s general credit balance during billable operations and triggers an off-session Stripe charge when it drops below a configured threshold — but only when the operation actually draws from general credits.
How the trigger works
Section titled “How the trigger works”The trigger fires inside record_operation_service after the credit waterfall completes. Five conditions must all be true:
auto_replenish_enabledistrueauto_replenish_pack_idis setauto_replenish_in_progressisfalse(no concurrent dispatch)- The operation drew from
included_creditsorpurchased_credits(general pools) - The new general balance (
included_credits + purchased_credits) is belowauto_replenish_threshold_credits
Condition 4 is the key refinement: orgs consuming only per-op-type pools (e.g. voice credits) never trigger auto-recharge, even when their general balance sits below threshold.
let op_drew_from_general = from_included > 0 || from_purchased > 0;let should_dispatch_replenish = auto_replenish_enabled && auto_replenish_pack_id.is_some() && !effective_in_progress && op_drew_from_general && new_general_balance < auto_replenish_threshold;The in_progress flag is set inside the existing SELECT FOR UPDATE transaction on org_budget, then the dispatcher is spawned fire-and-forget after the transaction commits.
Schema changes
Section titled “Schema changes”org_budget — new columns
Section titled “org_budget — new columns”| Column | Type | Default | Purpose |
|---|---|---|---|
auto_replenish_threshold_credits | integer | 0 | Balance floor that triggers a recharge |
auto_replenish_in_progress | boolean | false | Prevents concurrent dispatches |
auto_replenish_in_progress_started_at | timestamptz | null | Stuck-flag recovery (10-min timeout) |
auto_replenish_consecutive_failures | integer | 0 | Auto-disables at 3 consecutive failures |
credit_pack_purchase — new columns
Section titled “credit_pack_purchase — new columns”| Column | Type | Default | Purpose |
|---|---|---|---|
status | varchar | 'succeeded' | pending, succeeded, or failed |
failure_reason | varchar | null | Sanitized Stripe error code |
is_auto_replenish | boolean | false | Distinguishes auto from manual purchases |
A partial index covers the auto-recharge history query:
CREATE INDEX idx_credit_pack_purchase_is_auto_replenish ON credit_pack_purchase (organization_id, created_at DESC) WHERE is_auto_replenish = true;Dispatch flow
Section titled “Dispatch flow”dispatch_auto_replenish_service handles the Stripe charge:
- Looks up the org’s Stripe customer ID and calls
first_payment_method_idto get the first card on file (Stripe Billing Portal does not set a default invoice PM) - Inserts a
pendingcredit_pack_purchaserow withis_auto_replenish = true - Creates an off-session
PaymentIntentwithpayment_methodset explicitly and metadata includingpurpose: "auto_replenish"andorganization_id - On immediate Stripe decline: records a
failedpurchase, clearsin_progress, incrementsconsecutive_failures - On success: the
payment_intent.succeededwebhook grants credits (see below)
An outer wrapper guarantees auto_replenish_in_progress clears on any error path. If the flag is stale (older than 10 minutes), the next record_operation call clears it automatically.
Webhook handlers
Section titled “Webhook handlers”Two new handlers in dispatch_stripe_event_service:
payment_intent.succeeded — when metadata contains purpose = "auto_replenish":
lock_exclusivethe purchase row (prevents double-grant under Stripe redelivery)- Idempotency check on row
status(not thein_progressflag) - Grants credits to
purchased_credits - Clears
in_progress, resetsconsecutive_failuresto 0 - Sends a
BillingAutoRechargesuccess notification to org owners
payment_intent.payment_failed — same metadata gate:
- Records
failedstatus with sanitizedfailure_reason(allowlisted codes only) - Clears
in_progress, incrementsconsecutive_failures - At 3 consecutive failures: sets
auto_replenish_enabled = falseand notifies owners - Returns 500 on missing purchase row to force Stripe retry through the insert race window
API endpoints
Section titled “API endpoints”GET /api/billing/auto-replenish/status
Section titled “GET /api/billing/auto-replenish/status”Owner-only. Returns AutoReplenishStatus:
pub struct AutoReplenishStatus { pub enabled: bool, pub pack_id: Option<Uuid>, pub threshold_credits: i32, pub in_progress: bool, pub consecutive_failures: i32, pub has_payment_method: bool, pub current_balance_credits: i32, pub pack_price_cents: Option<i32>,}Degrades gracefully when the Stripe PM lookup fails — reports has_payment_method: false instead of returning a 500.
POST /api/billing/auto-replenish/settings
Section titled “POST /api/billing/auto-replenish/settings”Owner-only. Accepts UpdateAutoReplenishInput:
pub struct UpdateAutoReplenishInput { pub enabled: bool, pub pack_id: Option<Uuid>, pub threshold_credits: i32,}Validations: pack must exist and be active, threshold ≥ 0, payment method required when enabling. Uses lock_exclusive to prevent concurrent owner saves from racing. If enabling while balance is already below threshold, dispatches an immediate recharge inline.
Returns the fresh AutoReplenishStatus so the UI updates without a separate GET.
UI component
Section titled “UI component”AutoReplenishSection renders in the billing tab for org owners only:
- Toggle — enables/disables auto-recharge
- Threshold input — minimum credit balance before recharge fires
- Pack picker — which credit pack to purchase
- In-progress indicator — shows while a charge is pending
- Failure warning — yellow notice with consecutive failure count
- Disabled banner — red banner with “Update payment method” CTA when auto-disabled after 3 failures
- Recent attempts — history list of auto-recharge purchases with status
- Confirmation dialog — when enabling while below threshold, confirms immediate recharge
The UI polls in_progress on a 3-second interval until the webhook lands, using a generation counter to prevent duplicate concurrent polls.
Notifications
Section titled “Notifications”New BillingAutoRecharge category with a Wallet icon using the text-status-approved semantic token. Notifications without an attached entity route to /settings/billing on click.
notify_org_owners is now pub and in-app inserts are batched via insert_many (one round-trip instead of N).
Key files
Section titled “Key files”| File | Purpose |
|---|---|
migration/src/m20260501_131944_credit_auto_recharge.rs | Schema migration |
src/mods/billing/services/dispatch_auto_replenish_service.rs | Stripe PaymentIntent dispatch |
src/mods/billing/services/record_operation_service.rs | Trigger evaluation in credit waterfall |
src/mods/billing/services/dispatch_stripe_event_service.rs | Webhook handlers for succeeded/failed |
src/mods/billing/services/has_payment_method_service.rs | Stripe PM lookup |
src/mods/billing/api/get_auto_replenish_status_api.rs | Status endpoint |
src/mods/billing/api/update_auto_replenish_settings_api.rs | Settings update endpoint |
src/mods/billing/types/auto_replenish_settings_type.rs | Request/response types |
src/mods/billing/components/auto_replenish_section_component.rs | UI component |