Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fim.ai/llms.txt

Use this file to discover all available pages before exploring further.

FIM One ships a complete Stripe billing pipeline behind an admin-controlled feature flag. Private deployments without payment needs leave it off and never see the UI. SaaS operators flip one toggle and get hosted Checkout, Customer Portal, webhook-driven subscription lifecycle, and quota enforcement out of the box.
Billing is disabled by default. Fresh installs and existing self-hosts both start with system_settings.billing_enabled = FALSE. No billing UI surfaces until an admin explicitly activates it.

What you get

  • Free + Pro tiers with monthly token quotas (defaults: Free 1M / Pro 5M; both tunable post-activation)
  • Stripe-hosted Checkout — users upgrade without your code ever touching card data
  • Customer Portal — users update payment methods, download invoices, cancel — all on Stripe’s UI
  • Webhook-driven lifecycle — subscriptions provision and renew automatically; canceled subs demote to Free at period end
  • Quota enforcement — token usage tracked per period; mid-stream cutoff with a structured upgrade prompt
  • Admin pages for plan CRUD and subscription monitoring

Prerequisites

  1. Stripe account with Live mode activated. Singapore-incorporated companies must complete KYC (business UEN, director ID, bank account). Approval typically takes 1-3 days.
  2. Stripe Live API key of type Restricted (recommended over Standard sk_live_*** — easier to revoke, scoped permissions).
  3. Webhook endpoint publicly reachable at <your-domain>/api/webhooks/stripe.
  4. Bank account for payouts. Multi-currency settlement (e.g. USD payout to a USD account) is recommended for non-USD-default Stripe accounts to avoid 1.5-2% FX leakage per transaction.

Setup

1. Stripe Dashboard

Create the Pro product

  1. Catalog → Products → + Add product
  2. Name: Pro, description: 5M tokens / month, priority support
  3. Pricing: Recurring, monthly, $20.00 USD (adjust to your pricing strategy)
  4. Save → copy the resulting price_*** ID (you will UPDATE the local billing_plans table with this value after activation)

Create a Restricted API key

  1. Developers → API keys → + Create restricted key
  2. Name: fim-one production
  3. Permissions (minimum):
    • Customers: Write
    • Subscriptions: Write
    • Checkout Sessions: Write
    • Customer portal: Write
    • Prices: Read
    • Products: Read
  4. Save → copy rk_live_***

Register the webhook endpoint

  1. Developers → Webhooks → + Add endpoint
  2. URL: https://<your-domain>/api/webhooks/stripe
  3. Events to receive:
    • checkout.session.completed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.payment_succeeded
    • invoice.payment_failed
  4. After save, click “Reveal signing secret” → copy whsec_***
If your Stripe account default currency differs from the price currency you charge in (common case: SGD account charging USD):
  1. Settings → Bank accounts and currencies → Add a settlement currency
  2. Pick the price currency (e.g. USD)
  3. Attach the matching bank account (e.g. an Aspire USD virtual account)
  4. Save — Stripe routes USD charges directly to USD payouts, no FX conversion

2. Backend .env

Set these three keys in your production .env:
STRIPE_SECRET_KEY=rk_live_***
STRIPE_WEBHOOK_SECRET=whsec_***
STRIPE_BILLING_RETURN_URL=https://<your-domain>/settings?tab=billing
See Environment Variables for full reference. Restart the backend after editing .env so the keys are picked up:
./deploy.sh   # or: docker compose restart fim-one

3. Activate in Admin

  1. Log in as an admin
  2. Admin → System Settings → Billing
  3. Toggle Enable Stripe Billing ON
  4. The backend validates that both STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET are present — if either is missing it returns 400 and the toggle stays OFF
  5. On first activation only, the backend runs an idempotent setup:
    • Seeds Free + Pro plans (skipped if already present)
    • Sets system_settings.default_plan_id to the Free plan id
    • Backfills users.plan_id = free.id for any user without a plan
    • Syncs default_token_quota → Free plan monthly_token_quota so the existing admin-controlled global quota carries over
  6. Subsequent toggle off/on is a pure flag flip with no data side effects

4. Update the Pro plan with your live price

After activation, update the seeded Pro plan to point at your live Stripe price:
  • Admin → Billing → Plans → Pro → Edit
  • Paste your price_1*** (from step 1) into Stripe Price ID
  • Save
Or via SQL (if you prefer direct DB access):
UPDATE billing_plans
SET stripe_price_id = 'price_1***'
WHERE slug = 'pro';

5. Smoke test

  1. Open /settings?tab=billing as a regular user
  2. Click Switch to Pro
  3. Stripe Checkout opens; complete with a low-amount real card (refund afterwards)
  4. Webhook should fire — verify in Stripe Dashboard → Webhooks → recent events show 2xx responses
  5. Subscription row appears in subscriptions table; users.plan_id flips to pro
  6. UI now shows Pro plan + “Manage subscription” button

Disabling billing

Toggle the Enable Stripe Billing switch OFF in Admin → System Settings → Billing. When disabled:
  • All /api/billing/* endpoints return 503
  • The webhook endpoint returns 503 (Stripe will retry, then surface in Dashboard as failing — that’s fine, you can disable the webhook in Stripe Dashboard instead if billing is permanently off)
  • The Plan & Billing user-facing tab disappears
  • The Admin → Billing nav group is hidden
  • The quota chain skips the plan tier and falls back to default_token_quota directly
Data is preserved: existing subscriptions, billing_plans, and users.plan_id rows are untouched. Re-enabling resumes from the same state with no migration.

Calculation reference — quota & token math

This is the authoritative reference for every numeric rule that decides what a user is allowed to consume, when their counter resets, and how the resolution chain composes. Read this before changing pricing, adjusting quotas, building usage dashboards, or planning v2/v3 work. Future-but-not-yet-shipped rules are documented in their reserved slot so contributors know where new logic plugs in.

Glossary

VariableStorageSemanticsRange
users.token_quotaper-user (override)Three-state override; see semantics belowNULL, 0, or positive int
users.tokens_used_this_periodper-user (counter)Cumulative tokens since last resetnon-negative int
users.quota_reset_atper-user (anchor)Mirrors Subscription.current_period_end for paid userstimestamp
users.plan_idper-user (FK)Active planFK billing_plans.id
billing_plans.monthly_token_quotaper-planHard cap for users on this plannon-negative int
system_settings.default_token_quotasingletonDefensive fallback when no plan appliesnon-negative int
system_settings.default_plan_idsingletonFree-plan pointer for new/unassigned usersFK or NULL
system_settings.billing_enabledsingletonMaster switch — gates step 2 of the chainboolean

What counts as a token

Token consumption is accounted at the LLM call layer, sourced from LiteLLM’s usage object on every completion.
  • Counted: prompt tokens + completion tokens on every model call
  • Counted: every round-trip in a multi-step / tool-use agent flow (each model call is its own debit)
  • Counted: embedding requests (KB ingestion, retrieval scoring)
  • Not counted: input staged but never sent to a model (e.g. uploaded files that the user discards)
  • Not counted: requests that fail before reaching the provider (auth error, rate-limit pre-check)
  • Cached input: counted at full price in v1 (no provider cache discount is surfaced). v2 may credit cached prompt tokens separately.

Three-state override semantics

users.token_quota is the per-user administrative override. It carries three meanings in one column:
ValueMeaningUse case
NULLNot set — defer to plan / defaultDefault state for all normal users
0UnlimitedAdmin / internal accounts; “VIP gift”
N > 0Hard cap at NBlock an abuser without canceling their paid subscription; pre-paid enterprise allocation
The override always wins over plan and default. It exists so admins can pin individual users above or below their plan tier without touching Stripe.

Quota resolution chain — v1 (current)

For any authenticated request, the cap is computed top-down — first match wins:
1. users.token_quota        ── NULL? skip. 0? unlimited. N>0? cap at N.
2. users.plan.monthly_token_quota   ── only when billing_enabled = TRUE
3. system_settings.default_token_quota  ── defensive fallback
4. unlimited                ── last resort if everything above is NULL
Step 3 is rarely reached when billing is on, because activation backfills users.plan_id for every user. It exists as a defense-in-depth so a misconfigured plan never silently uncaps a user.

Period reset

  • For paid users, quota_reset_at mirrors Subscription.current_period_end. The invoice.payment_succeeded webhook handler sets tokens_used_this_period = 0 and advances quota_reset_at to the new period end on each successful renewal.
  • For Free users (no Stripe subscription), an hourly cron rolls tokens_used_this_period to 0 on a calendar-month boundary anchored to plan-assignment date.
  • Plan changes mid-period do not reset the counter — only renewals do. This prevents quota-cycling exploits (“subscribe → use Pro quota → cancel → subscribe again”).

Mid-stream enforcement

  • Pre-flight check at chat-call entry: cheapest path, blocks requests the user can’t afford to start.
  • During streaming, the running token count is re-evaluated on every chunk. Crossing the cap closes the stream with a structured terminator frame, not a network error.
  • The frontend interprets the terminator and surfaces <QuotaExceededDialog> with a deep link to /settings?tab=billing.
  • Non-streaming responses return HTTP 402 with body { code: "QUOTA_EXCEEDED", reset_at, upgrade_url }.

Billing-disabled fallback

When system_settings.billing_enabled = FALSE:
  • Step 2 of the chain is skipped — the chain collapses to override → default → unlimited.
  • /api/billing/* and /api/webhooks/stripe return 503.
  • Plan & Billing user tab and Admin → Billing nav group are hidden.
  • All billing data (subscriptions, plans, users.plan_id) is preserved — re-enabling resumes from the same state with no migration.

Reserved: quota chain v2 — Team seats

Not yet shipped. Documented here so v2 work has a known landing spot.
When the Team plan ships:
  • Subscription.quantity carries seat count (Stripe-native).
  • A user’s effective plan resolves through Team membership before falling back to their personal plan:
    effective_plan = team.plan if team_member(user) else user.plan
    
  • Quota is per seat (each Team member gets a full monthly_token_quota), not a pooled bucket. Pooled buckets create first-come-first-served exhaustion and are anti-customer.
  • Override semantics are unchanged — Team admins can still hard-cap individual members via users.token_quota = N, which sits above the team plan in the chain.

Reserved: quota chain v3 — native Org allocation (no Stripe)

Not yet shipped. Reserved for on-prem / enterprise deployments that allocate quota internally without paying Stripe per user.
  • New table org_quota_allocations(user_id, monthly_token_quota, org_id) distributes a parent budget across members.
  • Allocations are per user, not a shared pool — every member has a clear individual SLA.
  • Updated chain:
    override → max(plan_quota, org_allocation) → default → unlimited
    
  • max(), not sum(). A paid Pro user never gets less than they paid for, even if their Org admin sets a low allocation. Stripe-paid quota is sacrosanct.

Reserved: pay-per-use credit balance (v3 separate dimension)

Not yet shipped. A separate axis from the chain above — credits are a one-time top-up, not a subscription tier.
  • New table user_credits(user_id, balance_cents, currency) — funded via Stripe Checkout mode='payment'.
  • Consumption order: subscription quota first, then credit balance (only after subscription is exhausted does the credit decrement begin).
  • Credit balance is non-refundable (industry standard for prepaid).
  • UI exposes both bars: Subscription quota: 4.2M / 5M used + Credits: $7.40 remaining.

Default values

Ship-time defaults — all tunable post-install except where noted.
VariableDefaultTunable via
billing_plans.monthly_token_quota (Free)1,000,000Admin → Billing → Plans → Free → Edit
billing_plans.monthly_token_quota (Pro)5,000,000Admin → Billing → Plans → Pro → Edit
system_settings.default_token_quota1,000,000 (synced to Free at activation)Admin → System Settings → Quotas
system_settings.billing_enabledFALSEAdmin → System Settings → Billing
Pro list price$20.00 USD / monthStripe Dashboard (price object)
Stripe webhook events subscribed6Stripe Dashboard → Webhooks
Stripe price cache TTL5 minuteshardcoded in stripe_client.py
Subscription lifecycle cronhourlyAPScheduler in web/main.py
Free-tier reset cronhourly (calendar-month boundary)APScheduler in web/main.py

Pricing model

V1 is a flat subscription. Free + Pro, monthly billing, USD-only. Out of scope for v1 (intentional, deferred to roadmap):
  • Team plan (Stripe seats / subscription.quantity)
  • Annual billing
  • Multi-currency presentment
  • Coupons / promo codes
  • Tax handling (Stripe Tax integration — needs separate compliance review)
  • Usage-based metering / overage charges
  • Pay-per-use credit balance (one-time top-up)
See the roadmap for what’s planned next.

Troubleshooting

The activation endpoint requires both STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET to be set. Confirm they’re present in .env and that the backend was restarted after editing.
Either billing is disabled (toggle is OFF), or the request signature failed verification (mismatched STRIPE_WEBHOOK_SECRET). Check Stripe Dashboard → Webhooks → recent events for the actual error body.
The checkout.session.completed webhook didn’t reach your backend. Verify the endpoint URL in Stripe Dashboard matches <your-domain>/api/webhooks/stripe exactly, including the trailing path. Check Webhook recent deliveries for failures.
The seed migration writes a test-mode price ID. After activating production billing, update the Pro plan to use your live price_1*** via Admin → Billing → Plans → Pro → Edit, or via direct SQL UPDATE.
Configure your business branding in Stripe Dashboard → Settings → Branding. Add your logo, business name (e.g. “FIM Labs Pte. Ltd.”), and address. Stripe applies these to all auto-generated receipts and invoices.
If your Stripe account default currency differs from your charge currency, Stripe converts at every payout (1.5-2% spread). Add a matching settlement currency under Settings → Bank accounts and currencies, attach a same-currency bank account, and Stripe will route same-currency payments without conversion.