Skip to main content
Version: 2.x

Create your first metered charge

This tutorial creates an isolated Bursar tenant, publishes one pricing rule, grants prepaid credits, and records a usage charge. The final ledger entry proves which account was charged, why it was charged, and which balance resulted.

Prerequisites

Prepare these dependencies before continuing:

  • Python 3.12 or 3.13 for the Bursar command-line interface (CLI)
  • PostgreSQL 16 or newer with pg_partman 5.x and pg_jsonschema 0.3+ available to the migration role
  • A PostgreSQL connection string for a database you may modify
  • Node.js 22 or newer if you will use the TypeScript example

:::caution Use a development database

bursar migrate creates and secures the bursar schema. Run this tutorial against a development database, not an existing production tenant.

:::

1. Install the package and schema

Install the Python package with PostgreSQL support. The package supplies the CLI used by both SDK integrations.

Terminal
python -m pip install "bursar[postgres]"
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/bursar"
bursar migrate

TypeScript applications also install the Node.js package:

Terminal
npm install @zonastery/bursar

bursar migrate applies the ordered SQL baseline and records each file checksum. Re-running the command is a no-op. The command fails if an applied migration file has changed.

2. Create a tenant

Create a tenant and retain the returned universally unique identifier (UUID). Every store binds to one tenant before it reads or writes business data.

Terminal
bursar tenant create acme --display-name "Acme"
export BURSAR_TENANT_ID="018f7f5f-7b4a-7000-8000-000000000001"

Use the UUID printed by your command for BURSAR_TENANT_ID. The value above is an example.

3. Define and publish pricing

Save this minimal configuration as pricing.yaml. It defines one metered operation, one rate card, one credit bucket, and one plan.

pricing.yaml
version: 1
catalog:
default_plan: standard
pricing:
operations:
completion:
measures:
input_tokens: { unit: token }
dimensions:
model: { type: string }
rate_cards:
standard:
operations:
completion:
unmatched:
action: charge
charge:
type: per_unit
measure: input_tokens
rate: "0.25"
unit_size: "1000"
credits:
buckets:
purchased: { priority: 10 }
default_bucket: purchased
plans:
standard:
rate_card: standard
allowed_operations: [completion]

Validate the file before publishing it:

Terminal
bursar config validate pricing.yaml
bursar config set pricing.yaml --label "quickstart"
bursar config list

The schema rejects unknown fields. Exact decimal values such as rate and unit_size remain strings so parsing does not introduce floating-point values.

4. Construct the facade

Read the connection and tenant identifiers from the environment, then bind one store to the tenant.

import os
from bursar import Bursar, PostgresStore

database_url = os.environ["DATABASE_URL"]
tenant_id = os.environ["BURSAR_TENANT_ID"]

store = PostgresStore(database_url, tenant_id=tenant_id)
bursar = Bursar.create(credit_store=store)

Keep the store tenant-bound for its full lifetime. Never set a session-scoped tenant value on a shared connection pool.

5. Create and fund an account

Create a stable account identifier in your application database. Notify Bursar after signup, then post a purchase with a replay-safe idempotency key.

from decimal import Decimal

account_id = "11111111-1111-4111-8111-111111111111"
bursar.accounts.on_account_created(
account_id,
event_key="signup:11111111",
)
grant = bursar.credits.add_credits(
account_id,
Decimal("100.000000"),
entry_type="purchase",
idempotency_key="purchase:quickstart:11111111",
)
print(grant.entry_id, grant.new_balance)

The signup event assigns the standard plan from catalog.default_plan. The purchase appends one ledger entry and creates spendable credit in the purchased bucket.

6. Meter and charge usage

Record 800 input tokens. The rate card prices 1,000 tokens at 0.25 credits, so this call charges 0.20 credits.

from bursar.metrics import UsageMetrics

usage = UsageMetrics(
operation="completion",
measures={"input_tokens": Decimal("800")},
dimensions={"model": "example-model"},
)
charge = bursar.credits.deduct(
account_id,
usage,
idempotency_key="request:quickstart:0195",
)
print(charge.amount, charge.balance_after, charge.entry_id)

Call deduct again with the same idempotency key and payload. Bursar returns the original result without adding another ledger entry. Reusing the key with a different payload returns a conflict.

7. Verify the ledger

Read the canonical balance and the latest entries. The account should hold 99.800000 credits after the 0.200000 charge.

balance = bursar.credits.get_balance(account_id)
page = bursar.credits.list_ledger_entries(account_id, limit=10)

print(balance.balance)
for entry in page.items:
print(entry.entry_type, entry.amount, entry.balance_after)

The ledger is the accounting history. Do not maintain a second application-level balance counter.

Continue with a production workflow