Elwing uses Polar (polar.sh) as its billing/subscription provider. Billing is credit-based: users purchase subscriptions or top-ups via Polar, receive credits, and credits are consumed as the AI agent processes emails, chats, and memory operations.
| Concept | Description |
|---|---|
| Credits | Internal currency. Converted from token cost: credits = tokenCents × 12.5 |
| Billing Owner | Each space has one billing owner (user). All charges in that space bill to them. |
| Billing Account | Per-user row (user_billing_account) tracking credit_balance_credits, current_debt_credits, max_overage_credits |
| Charge Ledger | Append-only log of every credit deduction (billing_charge_ledger) |
| Subscription Credits Ledger | Append-only log of every credit grant (subscription_credits_ledger) |
User registers → ensurePolarCustomerForUser()
→ Creates Polar customer (externalId = userId)
→ Creates local user_billing_account row
→ Optionally grants 5,000 initial credits (negative-amount event ingested to Polar meter)
User clicks Subscribe → createPolarCheckoutUrlForUser()
→ Redirects to Polar-hosted checkout page
→ On success, Polar fires subscription/meter events
User clicks Top-Up → createPolarTopupCheckoutUrlForUser(productId)
→ Same flow, one-time product
Polar emits events (subscription.created, meter.credited, order.paid, etc.)
↓
syncSubscriptionEventsForUser() — called by cron or on-demand
→ Fetches all events from Polar Events API (paginated, since last sync)
→ For each meter.credited event:
recordSubscriptionCredits() → INSERT into subscription_credits_ledger
→ UPDATE user_billing_account.credit_balance_credits += N
→ Updates last_synced_polar watermark
Agent processes a trigger (email, chat, cron, memory op)
↓
getChargeableForSpace(context)
→ Looks up space → billing owner → billing account
→ Checks remaining = balance + maxOverage − debt
→ If remaining ≤ 0 → throws OutOfCreditsError
→ Returns chargeCredits() callback
↓
After AI call completes:
chargeCredits(usedCredits, tokenCents)
→ createBillingCharge() → INSERT into billing_charge_ledger
→ UPDATE user_billing_account (deduct balance, accrue debt if needed)
→ ingestChargeToPolar() → POST to Polar Events API (event: "lefos_credits_used")
→ markBillingChargePolarSynced()
startBillingSyncScheduler() — runs every 60s
→ listUnsyncedBillingCharges() (polar_synced_at IS NULL)
→ For each: ingestChargeToPolar() → markBillingChargePolarSynced()
This ensures charges that failed to sync inline (network errors, etc.) are eventually delivered.
OutOfCreditsError thrown
↓
Before giving up, replayPolarEventsForSpaceBillingOwner()
→ Re-syncs latest Polar events (in case new credits arrived)
→ If still out: sendOutOfCreditsReplyOncePerDay()
→ Sends email to requester (rate-limited 1/day via Redis)
→ Billing owner gets link to /account/billing
→ Non-owner gets "contact your garden owner" message
email · cron · chat · message · memory-extraction · memory-compaction · memory-reflection · herald-chat
user_billing_account— per-user balance, debt, overage cap, Polar sync cursorbilling_charge_ledger— every deduction (idempotent by key)subscription_credits_ledger— every credit grant (idempotent by key)space_billing_config— maps space → billing owner user