Credits System
Credits are the internal currency for AI usage. The accounting model is deliberately simple: 1 credit = 1 LLM token.
- Chat: charged
inputTokens + outputTokensreported by the provider, decremented once after the stream completes (no upfront reserve). - RAG embeddings & search: charged 1:1 with the embedding
usage.total_tokens. - A fast pre-flight check rejects requests below
aiConfig.minCreditsRequiredbefore the LLM call (returns 402). - Concurrent requests can both pass pre-flight; the DB
CHECK (credits_balance >= 0)constraint is the last-resort safety net (a failed post-stream debit is logged after the answer has been delivered; the database rejects the debit and never permits a negative balance).
Credit pack purchase modal with multiple options
Credits Architecture
Credits Sources Credits Usage
───────────────── ─────────────
┌─────────────────┐ ┌─────────────────┐
│ Subscription │ │ AI Chat │
│ Monthly Refill │──┐ ┌──│ Requests │
└─────────────────┘ │ │ └─────────────────┘
│ │
┌─────────────────┐ │ ┌───┐ │ ┌─────────────────┐
│ Credit Pack │──┼─▶│ $ │──┼──│ API Calls │
│ Purchase │ │ └───┘ │ └─────────────────┘
└─────────────────┘ │ │
│ │ ┌─────────────────┐
┌─────────────────┐ │ └──│ Agent │
│ Admin Manual │──┘ │ Tasks │
│ Adjustment │ └─────────────────┘
└─────────────────┘
accounts.credits_balanceDatabase Schema
The credits system uses the accounts.credits_balance column for the current balance, with the credit_transactions table providing a full audit trail. Each transaction records the amount, direction (credit/debit), reason, source, and timestamp. This dual-storage approach enables both fast balance lookups and complete transaction history.
Atomic Credit Operations
Credits are managed through two PostgreSQL RPC functions to ensure atomicity. The decrement_credits function subtracts from the balance and logs the transaction with a reason (e.g., "AI chat message"). The add_credits function increases the balance and logs with a source (e.g., 'subscription_refill', 'license_purchase'). Both functions operate within a transaction to prevent race conditions and maintain the audit trail in credit_transactions.
Real signatures from the billing sources under database/schema/billing/:
-- Adds credits to an account; returns the new balance.
add_credits(
p_account_id uuid,
p_amount integer,
p_source credit_source,
p_reason text default null,
p_metadata jsonb default '{}'::jsonb
) returns integer
-- Deducts credits; raises an insufficient-credit exception if funds are unavailable.
decrement_credits(
p_account_id uuid,
p_amount integer,
p_reason text default null,
p_metadata jsonb default '{}'::jsonb,
p_source credit_source default 'ai_usage'
) returns integerUsing Credits in Code
To deduct credits for an operation (e.g., an AI request), call the decrement_credits RPC with the account ID, amount, and a descriptive reason. Wrap calls in a try/catch — the SQL function rejects insufficient funds, and the repository maps that failure to CREDIT_LEDGER_INSUFFICIENT_CREDITS (it does not return a sentinel value). To grant credits (e.g., after a purchase or subscription refill), call add_credits with the account ID, amount, and source identifier.
Application ledger calls use CreditLedgerRuntime through core/billing/repositories/credit-ledger.ts and lib/database/credit-ledger-context.ts. The isolated app_financial_worker login from DATABASE_FINANCIAL_WORKER_URL executes the bounded private ledger functions with its own pool of one connection. The general app_runtime login cannot execute those ledger functions or directly update balances; provider-native client roles receive no business-ledger access.
Never UPDATE accounts SET credits_balance = ... directly. Every credit change MUST flow through these RPCs so a credit_transactions ledger row is written with source + reason + metadata. Direct writes break reconciliation, MRR, GDPR export, and refund clawback flows — this regression has shipped to main more than once (see .claude/rules/anti-patterns.md §A3).
Monthly Refill
For Stripe, the paid-invoice handler in lib/payments/providers/stripe/event-effects.ts refills the configured plan credits via add_credits with source 'subscription_refill'. Idempotency is enforced two ways:
- Immutable payment proof — the provider invoice is stored once through
payment_proof_keyand provider + environment external-ID uniqueness. Re-delivery cannot create another financial row. - Period entitlement key — the credit ledger key includes provider, subscription, configured catalogue item, and provider period start. The signed paid webhook owns this key for both natural and requested trial conversion, while the durable end-trial command only coordinates the one-shot provider mutation and convergence.
See .claude/rules/billing.md for the full event matrix.
Displaying Credits
The CreditDisplay component in components/billing/credit-display.tsx shows the current credit balance for the active account. It reads from accounts.credits_balance and updates in real time. The component is used in the dashboard header and billing pages to give users visibility into their remaining credits.
Credit Packs Purchase
Credit packs are one-time purchases that add credits to an account. They are defined in config/pricing.ts alongside subscription plans, with a catalogue binding for the server-selected provider. After that provider's signed paid event is journaled and normalized, the system records the provider-neutral payment and adds the purchased credits through the add_credits RPC. Checkout redirects and synchronous provider responses never grant credits.
Credit Transaction History
Every credit operation is recorded in the credit_transactions table, creating a complete audit trail. Transactions track the amount, balance after the operation, reason, and source. This data is displayed in the organization billing page and the admin dashboard for transparency and debugging.