:::info Executable tutorial
This page is generated from a tested Jupyter notebook. Open it in Google Colab or view the source notebook.
:::
Manage the credit lifecycle
This tutorial follows credits from purchase through metered spending, bucket allocation, refund, and ledger inspection. Every mutation runs against an isolated PostgreSQL store and uses the same transactional paths as a production integration.
Learning objectives
After completing this tutorial, you can:
- Post a purchase with a stable idempotency key
- Charge measured usage and handle insufficient credit
- Explain how bucket priority controls consumption
- Refund a charge and verify the append-only ledger
Prerequisites
- Complete the foundation tutorials
- Start the notebook server from
samples/python/notebooks/ - Permit the shared helper to create a disposable PostgreSQL environment
Setup
We start a temporary Postgres store, publish the standard pricing config, and create USER_ADA on the pro plan (unmetered usage billed from a prepaid balance — the free-plan allowance path is covered in notebook 05).
from decimal import Decimal
from bursar.metrics import UsageMetrics
from shared import cleanup, base_config, publish_config, start_postgres_store, USER_ADA
def completion(output=Decimal(500)):
return UsageMetrics(
operation="completion",
measures={
"input_tokens": Decimal(1000),
"output_tokens": output,
"cache_read_tokens": Decimal(200),
},
dimensions={"model": "gpt-4o"},
)
def execution(jobs=Decimal(1)):
return UsageMetrics(
operation="execution",
measures={"jobs": jobs, "compute_seconds": Decimal(30)},
dimensions={"model": "gpt-4o"},
)
store, pgdata = start_postgres_store()
bursar = publish_config(store, base_config())
bursar.accounts.on_account_created(USER_ADA, "signup")
bursar.credits.set_user_plan(USER_ADA, "pro")
print("temporary postgres:", pgdata)
Buying credits
add_credits lands money in the default purchased bucket and returns the resulting balance. The purchase also posts a purchase entry to the ledger and tracks the lifetime total. Expect new_balance = 100.000000 and lifetime_purchased = 100.000000.
result = bursar.credits.add_credits(
USER_ADA,
Decimal("100.00"),
entry_type="purchase",
idempotency_key="buy-topup-0001",
)
print("entry_id:", result.entry_id)
print("new balance:", result.new_balance)
print("lifetime purchased:", result.lifetime_purchased)
print("bucket:", result.bucket)
print("idempotent replay?:", result.idempotent)
Idempotent top-ups
Retrying the same purchase with the same idempotency_key is a no-op: the store returns the original entry id and marks the call idempotent=True. Network retries can never double-charge a user.
replay = bursar.credits.add_credits(
USER_ADA,
Decimal("100.00"),
entry_type="purchase",
idempotency_key="buy-topup-0001",
)
print("same entry_id:", replay.entry_id == result.entry_id)
print("balance unchanged:", replay.new_balance)
print("idempotent replay?:", replay.idempotent)
Spending on usage
A small chat completion is priced at 0.000008 credits (1000 input + 500 output + 200 cached tokens at the standard rate card). Metered deduct debits the balance and records a usage ledger entry. Replaying the same idempotency key later is a no-op that returns the stored result of the original charge — note its balance snapshot is from the original charge, not the current balance.
first = bursar.credits.deduct(USER_ADA, completion(), idempotency_key="chat-1")
print("entry:", first.entry_id[:12], "| amount:", first.amount)
print("balance after:", first.balance_after)
print("bucket breakdown:", first.bucket_breakdown)
bigger = bursar.credits.deduct(USER_ADA, completion(output=Decimal(20000)), idempotency_key="chat-2")
print("20k-output chat | amount:", bigger.amount, "| balance after:", bigger.balance_after)
replay = bursar.credits.deduct(USER_ADA, completion(), idempotency_key="chat-1")
print("replayed chat-1:", replay.idempotent, "| same entry:", replay.entry_id == first.entry_id)
print("replay result balance snapshot:", replay.balance_after)
print("actual balance now:", bursar.credits.get_balance(USER_ADA).balance)
When the money runs out
A metered charge that would push the balance negative raises InsufficientCreditsError (INSUFFICIENT_CREDITS) and charges nothing — 2500 execution jobs cost exactly 100.00 credits, but 2501 cost 100.04. The direct balance API, deduct_credits, surfaces the same situation as a StoreError from the store RPC. In both cases the balance is untouched.
try:
bursar.credits.deduct(USER_ADA, execution(jobs=Decimal(2501)), idempotency_key="run-over")
except Exception as exc:
print("metered deduct:", type(exc).__name__, getattr(exc, "code", None))
try:
bursar.credits.deduct_credits(
USER_ADA, Decimal("1000000"), idempotency_key="run-over:raw"
)
except Exception as exc:
print("direct deduct_credits:", type(exc).__name__, getattr(exc, "code", None))
print("balance untouched:", bursar.credits.get_balance(USER_ADA).balance)
Buckets and spillover
Credits live in buckets with a spending priority: promotional credits (priority 1) are spent before purchased credits (priority 10). A single charge may draw from several buckets — here a 0.08 execution charge drains the 0.05 promotional grant and spills 0.03 into purchased. Each bucket reports expires, the expiry-enabled flag of its definition in the published config (per-lot expiry is covered in notebook 07).
grant = bursar.credits.add_credits(
USER_ADA, Decimal("0.05"),
entry_type="grant", bucket="promotional", idempotency_key="promo-jul",
)
print("promo grant | new balance:", grant.new_balance)
spill = bursar.credits.deduct(USER_ADA, execution(jobs=Decimal(2)), idempotency_key="run-spill")
print("0.08 charge | amount:", spill.amount, "| breakdown:", spill.bucket_breakdown)
print("balance after:", spill.balance_after)
balances = bursar.credits.get_bucket_balances(USER_ADA)
for b in balances.buckets:
print(f"bucket={b.bucket_key!r} priority={b.priority} expires={b.expires} balance={b.balance}")
print("total:", balances.total_balance)
Refunds
refund_credits reverses a charge and returns money to the bucket it came from. Refunds can be partial via amount=; a full refund (the default) returns everything. Refunding the same entry twice is idempotent — the store returns the original refund entry rather than refunding again. Refunding a purchase entry — money that was never spent — is rejected by the store with a validation error.
refunded = bursar.credits.refund_credits(
bigger.entry_id, idempotency_key="refund:chat-2:full"
)
print("refund:", refunded.amount, "| original:", refunded.original_entry_id[:12], "| new balance:", refunded.new_balance)
partial = bursar.credits.refund_credits(
spill.entry_id, amount=Decimal("0.03"), idempotency_key="refund:run-spill:partial"
)
print("partial refund:", partial.amount, "| new balance:", partial.new_balance)
again = bursar.credits.refund_credits(
bigger.entry_id, idempotency_key="refund:chat-2:full"
)
print("second refund idempotent:", again.refund_entry_id == refunded.refund_entry_id, "| balance:", again.new_balance)
try:
bursar.credits.refund_credits(
result.entry_id, idempotency_key="refund:purchase:rejected"
)
except Exception as exc:
print("refund a purchase:", type(exc).__name__)
The ledger
Every credit event — purchases, grants, usage, refunds, expiries — is an append-only ledger entry. The most recent entries come first; note the refund entries referencing their original charges.
page = bursar.credits.list_ledger_entries(USER_ADA, limit=50)
print(f"{len(page.items)} entries, newest first:")
for e in page.items:
print(f" {e.entry_type:8s} {e.amount:>12} {e.created_at[:19]}")