:::info Executable tutorial
This page is generated from a tested Jupyter notebook. Open it in Google Colab or view the source notebook.
:::
Configure plans and allowances
Plans combine a rate card, allowed operations, entitlements, and optional recurring credit allowances. This tutorial moves one account from a free plan to a paid plan and back while observing admission and balance behavior.
Learning objectives
After completing this tutorial, you can:
- Assign and change an account plan
- Distinguish allowance consumption from balance deductions
- Enforce operation and feature entitlements
- Explain what happens when a plan changes mid-cycle
Prerequisites
- Complete the credit lifecycle tutorial
- Start the notebook server from
samples/python/notebooks/
Setup
Start a temporary Postgres store with the standard config. on_account_created lands USER_ADA on the free plan by default.
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")
print("created on plan:", bursar.credits.get_user_plan(USER_ADA).plan_key)
The free allowance
The free plan grants 10,000 credits per month. check_allowance reports the plan's allowance window: how much is left, and when the period runs. Nothing has been spent yet, so the full 10000.000000 remains.
allowance = bursar.credits.check_allowance(USER_ADA)
print("plan:", allowance.plan_id)
print("allowance remaining:", allowance.allowance_remaining)
print("period:", allowance.period_start, "->", allowance.period_end)
Spending the allowance
Free completions draw down the allowance, not a wallet: amount charged to the balance is 0.000000, the allowance consumed is 0.000008 per small chat, and no ledger entry is written (entry_id is empty). After 12 chats the allowance shows 9999.999904 remaining — 12 × 0.000008 consumed.
for n in range(12):
r = bursar.credits.deduct(USER_ADA, completion(), idempotency_key=f"free-{n}")
print(f"chat {n + 1:2d}: amount={r.amount} allowance_consumed={r.allowance_consumed} entry_id={r.entry_id!r}")
allowance = bursar.credits.check_allowance(USER_ADA)
print("allowance remaining after 12 chats:", allowance.allowance_remaining)
Entitlements: features behind the plan
get_user_plan exposes the entitlement map. On the free plan, voice_mode is False and max_context is 128000. Use check_feature as the runtime gate before offering a protected feature.
plan = bursar.credits.get_user_plan(USER_ADA)
print("plan key:", plan.plan_key)
print("entitlements:", plan.entitlements)
print("voice_mode:", plan.entitlements["voice_mode"].value, "| max_context:", plan.entitlements["max_context"].value)
print("check_feature(voice_mode):", bursar.credits.check_feature(USER_ADA, "voice_mode").has_feature)
Upgrading to pro
set_user_plan swaps the account to pro: voice_mode flips to True, max_context grows to 200000, and the allowance is gone — pro is billed from a prepaid balance instead, so check_allowance returns None — a plan with no credit allowance has no window at all.
bursar.credits.set_user_plan(USER_ADA, "pro")
plan = bursar.credits.get_user_plan(USER_ADA)
print("plan key:", plan.plan_key)
print("voice_mode:", plan.entitlements["voice_mode"].value, "| max_context:", plan.entitlements["max_context"].value)
print("allowance policy:", plan.allowance)
allowance = bursar.credits.check_allowance(USER_ADA)
print("check_allowance on pro:", repr(allowance))
Billing from the balance
The same completion that drew from the allowance on free now hits the wallet: amount=0.000008 plus a usage ledger entry. Pro also unlocks operations — an execution job is now billable at 0.040000.
bursar.credits.add_credits(USER_ADA, Decimal("100.00"), entry_type="purchase", idempotency_key="pro-topup")
chat = bursar.credits.deduct(USER_ADA, completion(), idempotency_key="pro-chat-1")
print("pro chat: amount", chat.amount, "| balance after", chat.balance_after, "| entry", chat.entry_id[:12])
run = bursar.credits.deduct(USER_ADA, execution(), idempotency_key="pro-run-1")
print("execution job: amount", run.amount, "| balance after", run.balance_after)
Downgrading mid-cycle
Back to free: the allowance policy is restored and picks up where it left off (9999.999904 — pro usage never touched it), voice_mode is gated again, and execution — an operation the free plan does not allow — is refused with OperationNotAllowedError.
bursar.credits.set_user_plan(USER_ADA, "free")
plan = bursar.credits.get_user_plan(USER_ADA)
print("plan key:", plan.plan_key)
print("allowance remaining:", bursar.credits.check_allowance(USER_ADA).allowance_remaining)
print("voice_mode:", bursar.credits.check_feature(USER_ADA, "voice_mode").has_feature)
try:
bursar.credits.deduct(USER_ADA, execution(), idempotency_key="downgraded-run")
except Exception as exc:
print("execution on free:", type(exc).__name__)