Skip to main content
Version: 2.x

Credit accounting model

Bursar records monetary state in one tenant-isolated PostgreSQL schema. This page explains the accounting entities and invariants; use the database schema and SDK references for exact tables and method signatures.

Accounts

A subject (typically one user) has one personal credit_accounts row, and optionally one account_kind = 'team' row per team. Each account carries a single locked canonical balance and a version that every mutation checks — the hot path never re-derives balance from history.

The append-only ledger

Every monetary event is one row in credit_ledger_entries: a signed amount, the exact balance_after after posting, an idempotency key, and a reference_entry_id for reversals. Kinds are grant, purchase, usage, expiry, revocation, refund, adjustment, reservation, release, and refund_clawback. Positive kinds post credits; usage, expiry, and reservation post debits; adjustment can go either way. A refund reverses its source: the refund entry restores the spent balance, and a refund_clawback entry reverses the original debits.

Lots and allocations

Every positive entry becomes a credit_lots row: the amount, its bucket, priority, and expiry. Debits consume lots in priority order — bucket tiering is credit_lot_allocations rows fanning out of the debit entry. When credits expire or are revoked, the remaining lot balance is allocated as expiry or revocation and the account balance moves by exactly that amount. Refunds restore the source lots that the original debit consumed, not the account globally, so expiry semantics survive refunds.

Leases

credit_leases are the atomic admission gate for long-running work: reserve captures a worst-case hold, a policy snapshot, and the minimum_balance at reservation; settle charges the actual cost and finalizes; release returns the hold. Entitlement, quota, and max_in_flight checks all happen in the same transaction as the reserve, so nothing drifts between "can I afford this?" and "you are billed".

Plans as state

Plans are non-monetary. account_plan_assignments records which plan an account holds against which catalog revision; allowance_windows and quota_windows track usage windows and consumption keyed by account. The allowance is real money available to the account, but it never enters the balance — DeductionResult.allowance_consumed tells you the split.

Usage charges

Each metered charge writes one compact credit_usage_charges row: operation, measures, model, requested, charged, allowance_requested, allowance_covered, and the pricing snapshot. The charged + allowance_covered = requested check is part of the schema. Zero-cost usage is still recorded, so quotas and analytics cannot be bypassed by a free rate.

Core tables

TablePurpose
credit_accountsOne locked balance and version per account (personal or team)
credit_ledger_entriesAppend-only money history with balance_after per entry
credit_lotsEach positive entry as a spendable lot with bucket, priority, expiry
credit_lot_allocationsWhich lots a debit consumed, in priority order
credit_leasesTemporary holds and policy snapshots for admission control
account_plan_assignmentsCurrent plan per account, pinned to a catalog revision

The three money invariants

  1. One locked balance per account. credit_accounts.balance is written only by the ledger-posting path, guarded by version; nothing mutates it directly.
  2. Balance equals the ledger. Every mutation appends an entry whose balance_after equals the resulting account balance — reading history and reading the balance always agree.
  3. The ledger is append-only and idempotent. Entries are never updated or deleted, and (account_id, idempotency_key) is unique, so a retried write replays the original entry instead of double-posting.

Transaction boundary

Every monetary mutation runs in one store transaction that locks the account before it checks idempotency, policy, and available value. Concurrent deductions therefore cannot authorize against the same balance snapshot, and application code never maintains a second financial counter.

Balances and availability

The account balance is the canonical posted value. Availability subtracts active lease holds, and bucket balances divide spendable credits by priority and expiry policy. Availability reads are suitable for display, but only an atomic deduction or reservation can authorize work under concurrency.

Use the credit lifecycle guide for balance and ledger procedures. Use the Python or TypeScript API reference for exact query signatures.