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_partman5.x andpg_jsonschema0.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.
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:
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.
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.
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:
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.
- Python
- TypeScript
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)
import { Bursar, PostgresStore } from "@zonastery/bursar";
const databaseUrl = process.env.DATABASE_URL!;
const tenantId = process.env.BURSAR_TENANT_ID!;
const store = new PostgresStore({
postgres: databaseUrl,
tenantId,
});
const bursar = new Bursar({ creditStore: 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.
- Python
- TypeScript
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)
const accountId = "11111111-1111-4111-8111-111111111111";
await bursar.accounts.onAccountCreated({
accountId,
eventKey: "signup:11111111",
});
const grant = await bursar.credits.addCredits(accountId, "100.000000", {
type: "purchase",
idempotencyKey: "purchase:quickstart:11111111",
});
console.log(grant.entryId, grant.newBalance);
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.
- Python
- TypeScript
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)
const usage = {
operation: "completion",
measures: { input_tokens: "800" },
dimensions: { model: "example-model" },
};
const charge = await bursar.credits.deduct(accountId, usage, {
idempotencyKey: "request:quickstart:0195",
});
console.log(charge.amount, charge.balanceAfter, charge.entryId);
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.
- Python
- TypeScript
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)
const balance = await bursar.credits.getBalance(accountId);
const page = await bursar.credits.listLedgerEntries(accountId, { limit: 10 });
console.log(balance.balance);
for (const entry of page.items) {
console.log(entry.entryType, entry.amount, entry.balanceAfter);
}
The ledger is the accounting history. Do not maintain a second application-level balance counter.
Continue with a production workflow
- Follow Manage the credit lifecycle for refunds, expiry, revocation, and cursor-based ledger reads
- Follow Protect long-running work for reserve, renew, settle, and release workflows
- Read Configuration and catalog revisions before adding plans, quotas, entitlements, or payment offers
- Run the executable tutorial collection to explore the same APIs against disposable PostgreSQL environments