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 migration-owner connection and permission to create two least-privilege login roles
  • Node.js 22 or newer if you will use the TypeScript example
  • Go 1.25 or newer if you will use the Go 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 BURSAR_MIGRATION_DATABASE_URL="postgresql://bursar_migrator:password@localhost:5432/bursar"
bursar migrate

As the migration owner, create separate operator and application logins. Set real passwords through your database provider or secret tooling rather than placing them in shell history:

PostgreSQL
CREATE ROLE bursar_app
LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS;
GRANT bursar_client TO bursar_app WITH INHERIT FALSE, SET TRUE;

CREATE ROLE bursar_ops
LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS;
GRANT bursar_operator TO bursar_ops WITH INHERIT FALSE, SET TRUE;

Give each login only the role shown above, then supply its connection through your normal secret manager. These illustrative local URLs keep the two trust boundaries explicit:

Terminal
export BURSAR_OPERATOR_DATABASE_URL="postgresql://bursar_ops:password@localhost:5432/bursar"
export DATABASE_URL="postgresql://bursar_app:password@localhost:5432/bursar"
export BURSAR_PROVIDER_ENVIRONMENT="test"

BURSAR_PROVIDER_ENVIRONMENT selects an isolated financial namespace. Use test for this tutorial and provider test credentials; production uses live.

TypeScript applications also install the Node.js package:

Terminal
npm install @zonastery/bursar pg

Go applications install the versioned module. The Python package still owns the migration CLI, so every SDK uses the same ordered SQL baseline:

Terminal
go get github.com/Zonastery/bursar/golang/v2

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 with an explicit universally unique identifier (UUID). Every store binds to one tenant before it reads or writes business data.

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

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"]
provider_environment = os.environ["BURSAR_PROVIDER_ENVIRONMENT"]

store = PostgresStore(
database_url,
tenant_id=tenant_id,
provider_environment=provider_environment,
)
bursar = Bursar(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.created_at)

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

Continue with a production workflow​