Skip to main content

06 - Financial Safety

bursar charges after an AI operation completes — but that is exactly when things can go wrong. If you only check the balance before the call and debit after it returns, you have a race: between the check and the debit, other concurrent operations can also pass the check, and a single expensive (or runaway agentic) call can finish costing far more than you estimated. Once the AI work has run, its cost is real regardless of what your ledger says — you cannot un-deliver a response.

The fix is an atomic lease taken before the work. A lease holds credits against available = balance − Σ(active holds), so concurrent operations actually see each other. Every operation follows the same shape: reserve (place a hold) → do the worksettle (charge the actual cost) or release (cancel, charging nothing). Admission is the only place limits are enforced; settle is de-clamped — it always bills the real cost, even if that exceeds the hold.

This notebook covers the whole model: the two billing shapes (dynamic inference vs. fixed-cost jobs), the two admission presets (strict_prepaid vs overdraft), the modifiers you can layer on top (free-tier allowances, concurrency limits, feature gates, low-balance alerts), the advisory reads for UI, refunds, and what happens when something goes wrong (expired or missing leases). All money is Decimal, and everything below runs on a real PostgreSQL store.

import time
import uuid
from decimal import Decimal

from bursar import (
CreditManager,
UsageMetrics,
LowBalanceConfig,
ConcurrencyLimitError,
FeatureNotEntitledError,
InsufficientCreditsError,
LeaseExpiredError,
LeaseNotFoundError,
)
from bursar.events import CreditEvent, CreditEventEmitter
from bursar.interface.models import PricingConfigData, PlanDefinition, OperationPolicy
from shared import start_postgres_store, cleanup

# A temporary Postgres cluster with the bursar schema already applied.
# PostgresStore uses UUID user ids, so every demo user below is a fresh uuid4().
store, pgdata = start_postgres_store()
print("✔ PostgresStore ready.")

Why leases exist

The problem described above, made concrete: two requests from the same user, arriving close enough together that neither has been charged when the other is admitted.

The naive way: check now, charge later

A common first instinct is to check the balance up front, do the work, and charge afterward with the atomic deduct_with_allowance primitive from Notebook 03. That primitive is safe against overdrawing any single charge — but it does nothing to stop two requests from both being told "yes, you have enough," because both check the same pre-charge balance.

naive_user = str(uuid.uuid4())
store.add_credits(naive_user, Decimal("50"), "signup_bonus")

worst_case = Decimal("40") # what either request might cost in the worst case

# NAIVE: check-then-act, with the charge deferred until after the (expensive,
# irreversible) work completes -- exactly the shape of "charge after the call returns".
balance = store.get_balance(naive_user).balance
request_a_admitted = balance >= worst_case # 50 >= 40 -> True
request_b_admitted = balance >= worst_case # both requests read the SAME pre-charge balance -> True
print(f" Both requests pass the balance check: A={request_a_admitted} B={request_b_admitted}")

# ... both AI calls run here -- compute cost already incurred, responses already
# delivered to both users, before either charge has been attempted ...

charge_a = store.deduct_with_allowance(naive_user, Decimal("38")) # A's actual cost
charge_b = store.deduct_with_allowance(naive_user, Decimal("35")) # B's actual cost
print(f" Charge A: amount={charge_a.amount} error={charge_a.error}")
print(f" Charge B: amount={charge_b.amount} error={charge_b.error}")
# B's charge is rejected -- but B's AI response was already generated and delivered.
# The 35-credit cost of that work is now unrecoverable: there is no way to
# retroactively un-deliver the response or force the charge through.
assert charge_b.error == "insufficient_credits"

The fix: reserve before doing any work

A lease moves the check to before the work, and makes it authoritative: reserve() atomically holds credits against available, so a second reserve() for the same user sees the first request's hold and is rejected immediately — before any AI call runs, before any cost is incurred.

lease_user = str(uuid.uuid4())
store.add_credits(lease_user, Decimal("50"), "signup_bonus")
lease_mgr = CreditManager(store=store, policy="strict_prepaid")
lease_mgr.publish_pricing_from_dict({"models": {"_default": "input_tokens * 1"}, "min_balance": 0})

lease_a = lease_mgr.reserve(lease_user, Decimal("40")) # holds 40 against available (50)
print(f" Request A reserved: held={lease_a.amount} available={lease_a.available}") # available now 10

# Request B arrives before A has settled. Its worst-case hold no longer fits --
# it is rejected HERE, before any AI call runs, before any cost is incurred.
try:
lease_mgr.reserve(lease_user, Decimal("40"))
print(" (unexpected) Request B admitted")
except InsufficientCreditsError:
print(" Request B rejected at admission -- no work was ever started for it")

lease_mgr.settle(lease_user, lease_a.lease_id, Decimal("38"))

Decision tree: which pattern for which situation?

SituationUseMethod
Cost is known upfront (flat fee: report, batch job, export)Single atomic deductiondeduct_fixed (or deduct_with_allowance from Notebook 03 for a raw amount)
Cost is only known after the work finishes (chat, agentic run)Reserve worst case, settle actualreserve()settle()/release()
Same as above, want less boilerplateOne-call wrapper over reserve/settle/releaserun_billed()
Trusted/paid users should be allowed to briefly go negativeA modifier on either of the abovepolicy="overdraft"
Free-tier monthly credits before touching balanceA modifier on either of the aboveplan free_allowance (Notebook 04), consumed automatically
Block a double-submit / cap concurrent work per userA modifier on reserve()max_concurrent (constructor or per-plan)
Restrict an operation to certain plansA gate on reserve()required_feature

1. strict_prepaid: reserve → settle, with worst-case sizing

We build a CreditManager with the default strict_prepaid policy and a small per-token price. The key discipline in strict mode: size the hold at the worst case you might bill. Admission subtracts that hold from available, so the balance can never be driven negative by work in flight. At settle time you bill the actual cost — which is usually less than the worst case, so the difference is automatically freed.

Below, a gpt-4 call is reserved at its worst-case token budget, then settled at the (smaller) actual usage.

# strict_prepaid is the default policy; min_balance=0 keeps the arithmetic obvious.
manager = CreditManager(store=store, policy="strict_prepaid")
manager.publish_pricing_from_dict({
"version": 1,
"models": {"gpt-4": "input_tokens * 0.01 + output_tokens * 0.03"},
"min_balance": 0,
})

user = str(uuid.uuid4())
manager.add_credits(user, Decimal("100"), "signup_bonus")

# Size the hold at the WORST CASE we might bill for this call.
worst_case = UsageMetrics(model="gpt-4", input_tokens=1000, output_tokens=1000) # cost 40
lease = manager.reserve(user, worst_case, operation_type="chat")
print(f" Lease: {lease.lease_id}")
print(f" Held: {lease.amount}") # 40 reserved against available
print(f" Available: {lease.available}") # 100 - 40 = 60 left for other work
print(f" Mode: {lease.billing_mode}")

# ... do the AI work ... it turned out cheaper than the worst case.
actual = UsageMetrics(model="gpt-4", input_tokens=500, output_tokens=200) # cost 11
ded = manager.settle(user, lease.lease_id, actual)
print(f" Charged: {ded.amount}") # 11, the ACTUAL cost (not the 40 hold)
print(f" Balance: {ded.balance_after}") # 100 - 11 = 89; the unused 29 was freed
assert ded.balance_after == Decimal("89.00")

Releasing a lease (work aborted)

If the work fails or is cancelled, call release instead of settle. Nothing is charged and the hold is returned to available. release is idempotent: calling it twice (or after a settle) is safe and reports a reason rather than raising.

lease = manager.reserve(user, Decimal("20"), operation_type="chat")
print(f" Reserved 20, available now: {manager.get_available(user).available}") # 89 - 20 = 69

# The operation failed — cancel the hold, charge nothing.
rel = manager.release(user, lease.lease_id)
print(f" Released: {rel.released} reason={rel.reason}")
print(f" Available restored: {manager.get_available(user).available}") # back to 89

# Idempotent: a second release does not raise, it just reports the state.
rel2 = manager.release(user, lease.lease_id)
print(f" Second release: released={rel2.released} reason={rel2.reason}")
assert manager.get_balance(user).balance == Decimal("89.00")

2. run_billed: the one-call shortcut

Wiring reserve → work → settle/release by hand is repetitive and easy to get wrong (e.g. forgetting to release on an exception). run_billed does it for you: pass an estimate (the worst-case hold) and a zero-arg do_work callable that returns (result, actual). On success it settles the actual cost; on any exception it auto-releases the lease and re-raises.

def do_work():
# Run the real operation, then report what it actually cost.
answer = "the AI response"
actual = UsageMetrics(model="gpt-4", input_tokens=300, output_tokens=100) # cost 6
return answer, actual

out = manager.run_billed(
user,
estimate=UsageMetrics(model="gpt-4", input_tokens=1000, output_tokens=1000), # worst-case hold 40
do_work=do_work,
operation_type="chat",
)
print(f" Result: {out['result']!r}")
print(f" Charged: {out['deduction'].amount}") # 6, the actual cost
print(f" Balance: {out['deduction'].balance_after}") # 89 - 6 = 83
assert out["deduction"].balance_after == Decimal("83.00")

Pricing setup for the rest of this notebook

The remaining sections model a small real platform with three tiers, a couple of fixed-cost jobs, and per-token model pricing — every section below shares this one config via saas_mgr.

  • Free — 100 credits/month allowance, 1 concurrent chat, no agentic.
  • Pro — 500 credits/month, 4 concurrent, agentic unlocked.
  • Paid — no allowance; paired with a dedicated overdraft-policy manager in Section 5.

The _default model expression (input_tokens * 1 + output_tokens * 1) gives 1 credit per token — easy mental arithmetic for the demos ahead.

saas_mgr = CreditManager(store=store)
saas_mgr.publish_pricing_from_dict({
"version": 1,
"models": {
"gpt-4o": "input_tokens * 0.01 + output_tokens * 0.03",
"_default": "input_tokens * 1 + output_tokens * 1",
},
"fixed": {
"daily_report": 10,
"batch_train": 50,
"quick_summary": 0.5,
},
"min_balance": 0,
"plans": {
"free": {
"id": "free", "name": "Free", "free_allowance": 100,
"max_concurrent": 1, "features": {"chat": True},
},
"pro": {
"id": "pro", "name": "Pro", "free_allowance": 500,
"max_concurrent": 4, "features": {"chat": True, "agentic": True},
},
"paid": {
"id": "paid", "name": "Paid", "free_allowance": 0,
"max_concurrent": 8, "features": {"chat": True, "agentic": True},
},
},
})
print("✔ Pricing published.")

3. Fixed-cost jobs: deduct_fixed

Operations with a known price — generating a PDF report, running a training job, sending a bulk notification batch — use deduct_fixed. The job name maps to the fixed section of your pricing config. There is no estimate/settle cycle: the cost is deducted atomically.

Critical ordering rule: deduct first, execute second. Unlike dynamic inference (where you must wait for the model to know the cost), a fixed-cost job's price is known up front — so the debit happens before the job starts. If you flip the order (run job → charge), a user can exhaust their balance mid-run and the completed work goes unpaid.

Job failure → refund. Because the charge is taken before execution, any failure in the job must be followed by refund_credits using the transaction_id from the DeductionResult. The example below shows the full success path and the failure path with refund.

deduct_fixed is idempotent when you pass an idempotency_key — retrying the same key returns the original result instead of double-charging.

batch_user = str(uuid.uuid4())
saas_mgr.add_credits(batch_user, Decimal("500"), "purchase")

# ── Success path ──────────────────────────────────────────────────────────────
# Step 1: deduct BEFORE starting the job.
idem_key = f"report-{batch_user}-2026-06-30"
result = saas_mgr.deduct_fixed(batch_user, "daily_report", idempotency_key=idem_key)
print(f"Report charged: {result.amount} credits (balance: {result.balance_after})")

# Step 2: run the job only after the deduction succeeds.
def generate_report():
return {"pages": 3, "status": "ok"} # simulated

report = generate_report()
print(f"Report generated: {report}")

# Retry with same key — idempotent, no double charge.
result2 = saas_mgr.deduct_fixed(batch_user, "daily_report", idempotency_key=idem_key)
print(f"Retry (idempotent): amount={result2.amount}, idempotent={result2.idempotent}, balance={result2.balance_after}")

# ── Failure path ──────────────────────────────────────────────────────────────
# Step 1: deduct before kicking off the training job.
train = saas_mgr.deduct_fixed(batch_user, "batch_train")
print(f"\nTraining job charged: {train.amount} credits (balance: {train.balance_after})")

# Step 2: job execution fails partway through.
def run_training():
raise RuntimeError("GPU node unreachable")

try:
run_training()
except RuntimeError as e:
print(f"Job failed: {e}")
# Step 3: refund the deduction — credits are restored to the user.
refund = saas_mgr.refund_credits(train.transaction_id, reason="training_job_failed")
assert refund.error is None, f"Refund failed: {refund.error}"
print(f"Refund issued: +{refund.amount} credits (balance: {refund.new_balance})")

4. max_concurrent: blocking a double-submit

An impatient user who clicks Send twice, or a client that retries before the first response arrives, creates two simultaneous leases for the same operation. max_concurrent caps the number of active leases per operation type per user. The second admission is rejected with ConcurrencyLimitError — before any model call is made.

The free plan already has max_concurrent: 1 for chat. The slot is freed when the first lease is settled or released. (You can also set a manager-wide default via CreditManager(..., max_concurrent=N) instead of per-plan; the per-plan value shown here takes precedence when the user has a plan.)

conc_user = str(uuid.uuid4())
saas_mgr.add_credits(conc_user, Decimal("200"), "signup_bonus")
saas_mgr.set_user_plan(conc_user, "free") # max_concurrent=1 for chat

# First request lands — lease acquired.
first = saas_mgr.reserve(conc_user, UsageMetrics(model="_default", input_tokens=100), operation_type="chat")
print(f"First request: lease acquired ({first.lease_id[:8]}…)")

# Second request (double-submit) — rejected immediately.
try:
saas_mgr.reserve(conc_user, UsageMetrics(model="_default", input_tokens=100), operation_type="chat")
except ConcurrencyLimitError as e:
print(f"Second request: ConcurrencyLimitError — {e}")

# First request finishes — slot freed, next request can proceed.
saas_mgr.settle(conc_user, first.lease_id, UsageMetrics(model="_default", input_tokens=80))
third = saas_mgr.reserve(conc_user, UsageMetrics(model="_default", input_tokens=100), operation_type="chat")
print(f"After settle: new lease acquired ({third.lease_id[:8]}…)")
saas_mgr.release(conc_user, third.lease_id)

5. The overdraft preset

Strict mode guarantees zero debt, but sometimes you want to let a trusted, paid user run past their balance and reconcile later — typically via a saved payment method that auto-charges on top-up. The overdraft preset admits down to a negative overdraft_floor, and at settle time clamps the actual charge so the balance never breaches that floor (C1): settle still bills the real cost of the work, but the debit itself is bounded. The balance genuinely goes negative (unlike strict_prepaid), but never past the floor you configured. Once past the floor, new admissions are rejected until you reconcile with add_credits.

This demo uses a dedicated CreditManager(policy="overdraft", overdraft_floor=...) instance rather than the shared saas_mgr from the previous sections — in an application with a small, fixed number of billing tiers, building one manager per policy (strict for free/pro, overdraft for paid) is the simplest approach; see the architecture doc if you need the policy resolved per-plan or per-call instead.

# A dedicated manager for overdraft users — floor matches the plan.
od_mgr = CreditManager(store=store, policy="overdraft", overdraft_floor=Decimal("-50"))
od_mgr.publish_pricing_from_dict({
"version": 1,
"models": {"_default": "input_tokens * 1 + output_tokens * 1"},
"min_balance": 0,
})

paid_user = str(uuid.uuid4())
od_mgr.add_credits(paid_user, Decimal("30"), "purchase") # thin balance

# Reserve an estimate — work may cost more.
lease = od_mgr.reserve(paid_user, Decimal("20"), operation_type="chat")
print(f"Estimated hold: {lease.amount} balance available: {lease.available}")

# Model returns 60 tokens — actual > estimate. settle bills the full 60 here
# because balance(30) - floor(-50) = 80 of headroom covers it; if the actual
# cost had exceeded that headroom, settle would clamp the debit to the floor (C1).
ded = od_mgr.settle(paid_user, lease.lease_id, Decimal("60"))
print(f"Actual charged: {ded.amount} balance after: {ded.balance_after}")

# Balance is now negative. New admission blocked until reconciled.
try:
od_mgr.reserve(paid_user, Decimal("5"), operation_type="chat")
except InsufficientCreditsError:
print("New request blocked — InsufficientCreditsError (balance below floor)")

# Auto-reload fires (card charged), recorded as a purchase-type top-up, balance reconciled.
od_mgr.add_credits(paid_user, Decimal("200"), "purchase")
after = od_mgr.get_available(paid_user)
print(f"After reload: balance={after.balance} available={after.available}")

# Now admission succeeds again.
new_lease = od_mgr.reserve(paid_user, Decimal("10"), operation_type="chat")
od_mgr.release(paid_user, new_lease.lease_id)
print("New request admitted successfully.")

6. Feature gating

Expensive or risky operations (autonomous agentic runs, bulk exports, higher-context models) should be restricted to plans that include them. Pass required_feature to reserve — bursar checks the user's plan and raises FeatureNotEntitledError before any work starts or credits are held.

free_gated = str(uuid.uuid4())
pro_user = str(uuid.uuid4())
saas_mgr.add_credits(free_gated, Decimal("500"), "signup_bonus")
saas_mgr.add_credits(pro_user, Decimal("500"), "purchase")
saas_mgr.set_user_plan(free_gated, "free")
saas_mgr.set_user_plan(pro_user, "pro")

# Free user — agentic feature absent from plan.
try:
saas_mgr.reserve(
free_gated,
UsageMetrics(model="_default", input_tokens=500),
operation_type="agentic",
required_feature="agentic",
)
except FeatureNotEntitledError:
print("Free user: FeatureNotEntitledError — agentic not in plan (no hold placed)")

# Pro user — feature present, admission succeeds.
pro_lease = saas_mgr.reserve(
pro_user,
UsageMetrics(model="_default", input_tokens=500),
operation_type="agentic",
required_feature="agentic",
)
print(f"Pro user: lease acquired — {pro_lease.amount} credits held")
saas_mgr.release(pro_user, pro_lease.lease_id)

7. Free-tier allowances as a modifier, not a separate pattern

Free-tier monthly allowances (Notebook 04) aren't a separate billing pattern — they're a modifier that composes with whatever pattern you're already using. The exact same reserve()/settle() calls from Section 1 automatically draw down a user's allowance first, falling back to their balance only once it's exhausted. DeductionResult.amount is the net amount actually debited from balance (after allowance); allowance_consumed is the portion covered by allowance instead — the total actual cost of the call is amount + allowance_consumed.

Below, a free-tier user (100 credits/month allowance) runs two chat calls. The first is covered entirely by allowance; once it's exhausted, the second is covered entirely by balance — same code path both times.

free_user = str(uuid.uuid4())
saas_mgr.add_credits(free_user, Decimal("200"), "signup_bonus")
saas_mgr.set_user_plan(free_user, "free")

allowance = saas_mgr.check_allowance(free_user)
print(f"Allowance remaining: {allowance.allowance_remaining} / period ends {allowance.period_end[:10]}")

# Call 1: fully covered by the 100-credit allowance.
lease = saas_mgr.reserve(free_user, UsageMetrics(model="_default", input_tokens=100, output_tokens=100), operation_type="chat")
ded = saas_mgr.settle(free_user, lease.lease_id, UsageMetrics(model="_default", input_tokens=60, output_tokens=40))
print(f"\nCall 1 — total cost: {ded.amount + ded.allowance_consumed} from allowance: {ded.allowance_consumed} from balance: {ded.amount}")
print(f"Allowance remaining: {saas_mgr.check_allowance(free_user).allowance_remaining}")

# Call 2: allowance now exhausted -- the SAME reserve()/settle() calls fall
# back to balance automatically, no branching required in application code.
lease2 = saas_mgr.reserve(free_user, UsageMetrics(model="_default", input_tokens=50, output_tokens=50), operation_type="chat")
ded2 = saas_mgr.settle(free_user, lease2.lease_id, UsageMetrics(model="_default", input_tokens=50, output_tokens=50))
print(f"\nCall 2 — total cost: {ded2.amount + ded2.allowance_consumed} from allowance: {ded2.allowance_consumed} from balance: {ded2.amount}")
print(f"Balance after: {ded2.balance_after}")

8. Multi-level low-balance alerts + LowBalanceConfig

Pass a low_balance=LowBalanceConfig(thresholds=[...], on_trigger=...) config to the constructor to get edge-triggered alerts: thresholds is sorted internally high → low, and each level fires once on the descent that crosses it, re-arming only after a top-up climbs back above it. on_trigger is an async-safe, non-blocking callable — perfect for a payment-provider-agnostic reload or notification. A handler that raises never breaks the operation.

We use the overdraft preset here only so the balance can descend freely through every threshold.

fired = []

def on_low_balance(event: CreditEvent):
# Payment-provider-agnostic: enqueue a reload, send an email, page on-call, etc.
level = event.data["threshold"]
balance = event.data["balance"]
fired.append(level)
print(f" [hook] balance {balance} crossed threshold {level} — triggering reload")

emitter = CreditEventEmitter()
lb_mgr = CreditManager(
store=store,
emitter=emitter,
policy="overdraft",
overdraft_floor=Decimal("0"),
low_balance=LowBalanceConfig(
thresholds=[Decimal("50"), Decimal("20"), Decimal("10")],
on_trigger=on_low_balance,
),
)
lb_mgr.publish_pricing_from_dict({
"version": 1,
"models": {"_default": "input_tokens * 1"},
"min_balance": 0,
})

lbu = str(uuid.uuid4())
lb_mgr.add_credits(lbu, Decimal("100"))

def charge(amount):
lease = lb_mgr.reserve(lbu, Decimal(amount))
lb_mgr.settle(lbu, lease.lease_id, Decimal(amount))

charge(55) # 100 -> 45 : crosses 50
charge(30) # 45 -> 15 : crosses 20
charge(7) # 15 -> 8 : crosses 10
print(f" Thresholds fired (each once): {fired}")
assert fired == [Decimal("50"), Decimal("20"), Decimal("10")]

9. Advisory reads: get_available() vs can_afford()

For UI elements — a balance bar, deciding whether to enable the Send button — use the non-locking advisory reads. They are fast, never block the operation, and may be slightly stale under concurrency; that's fine for UI. Only reserve() is the real admission gate.

  • get_available()balance, reserved (active lease holds), available = balance − reservedcash only, it does not include free allowance.
  • can_afford(amount_or_metrics)affordable, spendable, worst_case, reason. spendable is the user's effective spending powerbalance − reserved + remaining free allowance — which matches exactly what reserve() will actually admit. Use spendable (not available) to gate a "Send" button, so a free-tier user with allowance left isn't shown a false "insufficient funds."

available and spendable answer different questions — don't confuse them: available is what a raw balance display should show; spendable is what an admission-affecting UI decision should check.

ui_user = str(uuid.uuid4())
saas_mgr.add_credits(ui_user, Decimal("150"), "purchase")

# Simulate one in-flight lease already holding 80 credits.
in_flight = saas_mgr.reserve(ui_user, Decimal("80"), operation_type="chat")

# Balance bar: show how much is available for new work (cash only, no allowance).
avail = saas_mgr.get_available(ui_user)
print("Balance bar:")
print(f" Total balance: {avail.balance}")
print(f" In-flight holds: {avail.reserved}")
print(f" Available now: {avail.available}")

# Send-button affordability check. check.spendable includes remaining allowance —
# it is the effective spending power that reserve() will actually admit against,
# which is why it's the right field to gate a "Send" button (not avail.available above).
next_request = UsageMetrics(model="_default", input_tokens=200, output_tokens=100) # worst-case 300
check = saas_mgr.can_afford(ui_user, next_request)
print(f"\nSend button enabled: {check.affordable}")
print(f" Worst-case cost: {check.worst_case}")
print(f" Spendable: {check.spendable}")

# Request that would exceed available funds.
too_big = saas_mgr.can_afford(ui_user, Decimal("200"))
print(f"\n200-credit request affordable: {too_big.affordable} (reason: {too_big.reason})")

saas_mgr.release(ui_user, in_flight.lease_id)

10. Refunds

Refunds reverse a completed deduction and restore the user's balance. They are identified by transaction_id from the DeductionResult. Partial refunds are supported. result.error is set (not raised) on business-rule failures such as over-refunding, duplicate refunds, or refunding a purchase — always check it before treating the result as success.

ref_user = str(uuid.uuid4())
saas_mgr.add_credits(ref_user, Decimal("500"), "purchase")

# Charge for a batch training job.
ded = saas_mgr.deduct_fixed(ref_user, "batch_train")
print(f"Charged: {ded.amount} (tx: {ded.transaction_id[:8]}… balance: {ded.balance_after})")

# Full refund — training job failed before completing.
refund = saas_mgr.refund_credits(ded.transaction_id, reason="training_job_failed")
assert refund.error is None, f"Refund failed: {refund.error}"
print(f"Full refund: +{refund.amount} new balance: {refund.new_balance}")

# Partial refund example — dynamic inference, bill only for tokens actually processed.
lease = saas_mgr.reserve(ref_user, Decimal("100"), operation_type="chat")
ded2 = saas_mgr.settle(ref_user, lease.lease_id, Decimal("80"))
print(f"\nCharged (inference): {ded2.amount} balance: {ded2.balance_after}")

partial = saas_mgr.refund_credits(ded2.transaction_id, amount=Decimal("30"), reason="user_cancelled_mid_stream")
assert partial.error is None
print(f"Partial refund: +{partial.amount} new balance: {partial.new_balance}")

# Duplicate refund — business-rule failure, never raises.
dup = saas_mgr.refund_credits(ded.transaction_id) # already fully refunded
print(f"\nDuplicate refund: error='{dup.error}' (no credits moved)")

11. Failure modes: expired and missing leases

Two things you will hit in production, both raised as clear typed exceptions rather than silent no-ops or generic errors:

  • LeaseExpiredError — you called settle() (or renew()) after the lease's TTL elapsed. The default TTL is generous (long enough for a normal request), but a stuck job or a very slow stream can outlive it. No charge is made; you would typically reserve() again and retry, or renew() before the TTL runs out on long-running work.
  • LeaseNotFoundError — you called settle() or release() with a lease_id that doesn't exist, belongs to another user, or was already released. This is different from release()'s own idempotency (Section 1) — releasing twice is safe and reports a reason; settling an already-released lease raises instead, since silently accepting a charge against a hold that no longer exists would be a real bug to hide.
  • Refund failures (over-refund, duplicate refund) never raise — refund_credits() sets result.error instead, exactly as shown with the duplicate refund in Section 10 and in Notebook 03. It's the one departure from "business failures raise typed exceptions" in this notebook, and it's intentional: a refund is often called from a webhook or a retry path where you want to inspect and log the outcome rather than handle an exception.
fail_user = str(uuid.uuid4())
saas_mgr.add_credits(fail_user, Decimal("500"), "purchase")

# LeaseExpiredError: settling after the lease's TTL has elapsed.
short_lease = saas_mgr.reserve(fail_user, Decimal("10"), operation_type="chat", ttl=1)
time.sleep(1.2)
try:
saas_mgr.settle(fail_user, short_lease.lease_id, Decimal("5"))
except LeaseExpiredError:
print(" settle() after TTL elapsed: LeaseExpiredError (no charge was made)")

# LeaseNotFoundError: settling a lease that's already been released.
lease2 = saas_mgr.reserve(fail_user, Decimal("10"), operation_type="chat")
saas_mgr.release(fail_user, lease2.lease_id)
try:
saas_mgr.settle(fail_user, lease2.lease_id, Decimal("5"))
except LeaseNotFoundError:
print(" settle() on an already-released lease: LeaseNotFoundError")

# Refund failures never raise -- see the duplicate refund_credits() call in Section 10.
print(" (duplicate/over-refund: see the refund_credits().error check in Section 10)")

Recap

Use casePatternKey method
Dynamic inference (unknown cost until the work finishes)reserve → model call → settle (or release on error)manager.reserve() / manager.settle()
One-call dynamic inferencerun_billed auto-wires reserve/settle/releasemanager.run_billed()
Fixed-cost job (known price, possibly fractional)Single atomic deduct, idempotent, does not consume allowance by default (use_allowance=True opts in)manager.deduct_fixed()
Free monthly creditsPlan with free_allowance (+ optional allowance_period, Notebook 04); consumed automatically by reserve/settle/deduct_fixedmanager.check_allowance()
Double-submit preventionManager-wide or per-plan max_concurrentRaises ConcurrencyLimitError
Plan-gated featuresPass required_feature to reserveRaises FeatureNotEntitledError
Balance bar / send buttonget_available().available is cash-only; can_afford().spendable includes allowance headroom — use spendable for the buttonmanager.get_available() / manager.can_afford()
Trusted paid users, auto-reloadOverdraft preset + low_balance=LowBalanceConfig(...) hookpolicy="overdraft" + manager.add_credits()
Reverse a chargeRefund by transaction ID, supports partial, never raisesmanager.refund_credits()

Error reference:

  • InsufficientCreditsError — balance + allowance (minus active holds) below floor at admission
  • ConcurrencyLimitErrormax_concurrent active leases for this op type already
  • FeatureNotEntitledError — user's plan missing required_feature
  • CapReachedError — spend cap (deny) hit at admission (Notebook 07)
  • LeaseExpiredError — lease TTL elapsed before settle
  • LeaseNotFoundErrorlease_id unknown, belongs to another user, or already released

Event reference (non-blocking signals after a charge):

  • credits.overdraft — balance went negative (overdraft mode)
  • credits.floor_breach — balance slipped below min_balance without going negative (strict mode under-estimate)
  • credits.low_balance — edge-triggered threshold crossing (configure via low_balance=LowBalanceConfig(thresholds=..., on_trigger=...); defaults to a single threshold of min_balance * 2, which is 0 unless you set min_balance or low_balance explicitly)
  • credits.cap_warning — soft spend-cap crossed at settle time (Notebook 07)

See also: Financial Safety guide.

cleanup(pgdata)