Skip to main content

00 - Concepts & Setup

Before diving into any single feature, it helps to see the whole shape of bursar: four pieces that compose into a full credit-billing system for an AI product. This notebook introduces them, runs the smallest possible example against each, and explains the setup code that every later notebook relies on — so those notebooks can skip straight to the feature being taught instead of re-explaining their own scaffolding.

  • PricingEngine — a stateless calculator. Give it usage metrics (input/output tokens, tool calls, search queries, …) and a set of formulas, and it returns a cost. It never touches a database and produces the same answer every time for the same inputs.
  • CreditStore — the persistence layer. An abstract interface with three shipped implementations: MemoryStore (in-process, for tests and these notebooks), PostgresStore (production), and SupabaseStore. It owns balances, leases, plans, spend caps, and analytics.
  • CreditManager — the object your application code actually calls. It wires a PricingEngine and a CreditStore together and adds the operational layer on top: the lease lifecycle, allowance tracking, spend caps, and an event system.
  • A credit — an opaque unit of value that you define. Most SaaS platforms map credits to a currency at the UI layer (for example, 1 credit = $0.001); bursar itself is currency-agnostic.

Everything in this series builds on these four pieces, one capability at a time — starting with PricingEngine (Notebooks 01–02), then CreditStore/CreditManager operations (Notebooks 03 onward).

The smallest possible example

You don't need a pricing engine or a database to start experimenting with balances — MemoryStore runs entirely in-process and needs no setup beyond store.setup(). It's the same store every later notebook falls back to whenever a feature doesn't specifically require Postgres.

from decimal import Decimal
from bursar.interface.memory import MemoryStore

store = MemoryStore()
store.setup()

user = "demo-user"

# Add credits: every deposit gets a transaction_id and a `type` label for audit purposes.
r = store.add_credits(user, 1_000, type="signup_bonus")
print(f"Balance after signup bonus: {r.new_balance}")

# Charge credits: deduct_with_allowance() is the atomic "calculate cost, then charge" primitive.
ded = store.deduct_with_allowance(user, Decimal("150"))
print(f"Balance after a 150-credit charge: {ded.balance_after}")
assert ded.balance_after == Decimal("850")

Why most other notebooks start with start_postgres_store()

From here on, most notebooks use PostgresStore instead of MemoryStore, because several features (analytics, teams, and the RPC-backed atomicity guarantees) are demonstrated against the real production store. Spinning up a real Postgres server isn't something you want to explain in every notebook, so it's factored into two helpers in shared.py, next to these notebooks:

  • start_postgres_store() initializes a throwaway Postgres data directory (initdb), starts a postgres process on a free local port, creates a database, and calls store.setup() — which runs every numbered SQL migration in bursar/sql/ (the tables and RPC functions every CreditStore method calls into). It returns (store, pgdata).
  • cleanup(pgdata) stops that process and deletes the temporary data directory.

You'll see this pair open and close nearly every remaining notebook:

from shared import start_postgres_store, cleanup
store, pgdata = start_postgres_store()
...
cleanup(pgdata)

None of this exists in a real deployment. In production you point PostgresStore at your actual database URL and run migrations once (bursar migrate <dsn>, covered in Notebook 12), not per session.

Store capability differences

CapabilityMemoryStorePostgresStore
Core: balances, atomic charge, refunds, leases, plans, allowance checks, spend-cap checks, expiry sweep
Analytics: spend_by_user, spend_by_model, top_users, daily_spend, aggregate_stats
Teams: create_team, add_team_member, deduct_team, get_team_members
Writing a new spend cap✓ (set_spend_cap)not part of the interface — see below
Assigning a plan to a user✓ — no manual seeding needed, see below

Two of these are worth stating precisely, because it's easy to read them as store limitations when they're really just where a piece of configuration lives:

  • Writing a spend cap is not part of the portable CreditStore interface. MemoryStore.set_spend_cap() is a convenience specific to that store, meant for tests and these notebooks. PostgresStore implements the read side (check_spend_cap, which deduct_with_allowance also calls internally) against a credit_spend_caps table, but there's no writer method — in production you insert a row into that table yourself (via a migration or your own admin tooling). Notebook 07 shows both sides.
  • Plans need no separate seeding step. Publishing a pricing config that includes a plans section — via store.set_active_pricing() or bursar pricing set — automatically upserts those plans into Postgres's credit_plans table as part of the same call. Notebook 04 shows this end to end.

With the shared vocabulary and setup out of the way, Notebook 01 starts with the first concrete piece: turning usage metrics into a cost.