Skip to main content
Version: 2.x

Credits service

bursar.credits owns credit balances, usage charging, the lease lifecycle, plans, ledger history, analytics, and team charging. Every method is synchronous, and every monetary amount is a Decimal quantized to six decimal places with half-up rounding. The account model is bucket-based: credits live in named buckets (e.g. promotional priority 1, purchased priority 10) and are consumed highest-priority-first.

Balances and adjustments

from decimal import Decimal

grant = bursar.credits.add_credits(
user_id, Decimal("100"), entry_type="purchase", idempotency_key="purchase:1"
)
charge = bursar.credits.deduct_credits(
user_id, Decimal("5"), idempotency_key="operation:1"
)
refund = bursar.credits.refund_credits(
charge.entry_id, reason="clawback", idempotency_key="refund:operation:1"
)

add_credits and deduct_credits move a raw amount; refund_credits reverses a prior entry. Every successful monetary result identifies its canonical ledger row (entry_id for adjustments, refund_entry_id for refunds). get_balance, get_available, and get_bucket_balances are advisory reads.

Idempotency

add_credits, deduct_credits, and refund_credits each require a caller-stable idempotency_key: retrying with the same key replays the original result instead of double-applying. deduct_credits threads its key through to the store, so administrative deductions are replay-safe too.

The high-volume path, however, is deduct(metrics, idempotency_key) — see below.

MethodParametersReturns
add_creditsuser_id: str, amount: Decimal | int | str, idempotency_key: str, entry_type="adjustment", metadata=None, expires_at=None, bucket=NoneAddCreditsResult
deduct_creditsuser_id: str, amount: Decimal | int | str, *, entry_type="adjustment", bucket, metadata, idempotency_keyAddCreditsResult
refund_creditsentry_id: str, amount=None, reason, metadata, idempotency_keyRefundResult
get_balanceuser_id: strBalanceResult
get_availableuser_id: strAvailableResult
get_bucket_balancesuser_id: strBucketBalancesResult

Metered charging

deduct(user_id, metrics, idempotency_key, metadata=None, *, feature=None) prices a UsageMetrics event with the active pricing engine and charges the cost in one atomic, idempotency-keyed transaction. The idempotency key is the replay guard for metered usage: the same key returns the same DeductionResult instead of double-charging.

result = bursar.credits.deduct(
user_id,
UsageMetrics(
operation="completion",
measures={"input_tokens": 500, "output_tokens": 200},
dimensions={"model": "gpt-4o"},
),
idempotency_key="request:9f8c",
)

deduct enforces the user's plan: unentitled operations, allowance limits, and quotas can block the charge. feature additionally requires a plan feature. deduct_flat_job and grant_subscription_cycle are specialized siblings for flat-rate jobs and billing-cycle grants. revoke_credits_by_entry_type removes all remaining lots sourced by one matching ledger operation; it is a broad cohort operation, not a single-transaction refund. sweep_expired_credits(dry_run=False) expires eligible credit lots.

MethodParametersReturns
deductuser_id: str, metrics: UsageMetrics, idempotency_key: str, metadata=None, *, feature=NoneDeductionResult
deduct_flat_jobuser_id: str, job_name: str, idempotency_key: str, metadata=None, feature=NoneDeductionResult
revoke_credits_by_entry_typeuser_id: str, entry_type: strRevokeCreditsResult
sweep_expired_creditsdry_run=FalseSweepResult

AI and batch usage accounting

Bursar records financial events, not workflow execution state. Charge a fixed-price workload once with deduct_flat_job; keep its stages, retries, latency, model calls, and parent/child trace structure in your workflow store and OpenTelemetry backend. For dynamically priced inference, use deduct when the amount is known up front or a lease when the final provider usage is known only after completion.

Each deduction or lease settlement creates a canonical credit_usage_charges receipt. Put only bounded values needed for pricing and reconciliation in its measures, dimensions, and metadata. Use record_usage only when you intentionally need a priced receipt without another account debit, such as usage billed by an external system; it is not a tracing API.

Detailed receipt payloads use monthly PostgreSQL partitions by default and follow the configured retention policy. When ClickHouse is enabled, those payloads and usage analytics are projected there instead. Operational AI telemetry—including prompts, completions, latency, retries, errors, and router decisions—belongs in OpenTelemetry/Langfuse.

Leases

When the final cost is only known after the work runs, reserve the estimate, do the work, then settle the actual cost. reserve is the only admission gate; a lease whose TTL expires before settlement is released by the store's reaper. settle bills the actual amount and finalizes the lease; release returns an unused hold; renew extends an active lease without changing its captured policy snapshot.

run_billed(user_id, options) wraps reserve → work → settle in one call: options.estimate prices the hold, options.do_work runs the operation and returns (result, actual), and any exception from do_work releases the lease and re-raises. Long jobs may call renew from inside do_work; a crash between reserve and settle is covered by the lease TTL.

from bursar.credits.service_types import ReserveOptions, SettleOptions

lease = bursar.credits.reserve(
user_id,
UsageMetrics(operation="completion", measures={"input_tokens": 1000}),
ReserveOptions(idempotency_key="run:7:reserve", ttl=300),
)
# ... do the work ...
bursar.credits.settle(
user_id,
lease.lease_id,
actual_metrics,
SettleOptions(idempotency_key="run:7:settle"),
)
MethodParametersReturns
reserveuser_id: str, metrics_or_amount, options: ReserveOptionsLeaseResult
settleuser_id: str, lease_id: str, metrics_or_amount, options: SettleOptions | NoneDeductionResult
releaseuser_id: str, lease_id: strReleaseResult
renewuser_id: str, lease_id: str, ttl: int | None = NoneLeaseResult
run_billeduser_id: str, options: RunBilledOptionsRunBilledResult

ReserveOptions carries idempotency_key, operation_type, billing_mode, ttl, metadata, feature, and model; SettleOptions carries idempotency_key, metadata, and feature. A caller-supplied settlement key is validated like every other replay key; if it is omitted, Bursar derives the stable key from the lease ID because a lease can settle only once.

Plans

set_user_plan(user_id, plan_key, plan_assigned_at=None) assigns a plan from the active catalog and emits credits.plan_changed. get_user_plan returns the current plan. check_allowance reports the plan's credit allowance (bucket + window) state, and check_feature resolves a feature value against the plan's entitlements.

bursar.credits.set_user_plan(user_id, "pro")
allowance = bursar.credits.check_allowance(user_id)
voice = bursar.credits.check_feature(user_id, "voice_mode")
MethodParametersReturns
set_user_planuser_id: str, plan_key: str, plan_assigned_at=NoneSetUserPlanResult
get_user_planuser_id: strGetUserPlanResult
check_allowanceuser_id: strAllowanceResult
check_featureuser_id: str, feature: strCheckFeatureResult

Ledger and usage charges

All history views share one cursor contract: stable (created_at, entry_id) ordering, cursor-only pagination, and no offset support. list_ledger_entries is the full account ledger; list_usage_entries filters it to usage entries; list_usage_charges returns metered usage charges (including allowance-covered events).

page = bursar.credits.list_ledger_entries(user_id, limit=50)
entry = bursar.credits.get_ledger_entry(user_id, page.items[0].entry_id)

while page.next_cursor:
page = bursar.credits.list_ledger_entries(
user_id, limit=50, cursor=page.next_cursor
)
MethodParametersReturns
list_ledger_entriesuser_id: str, entry_types=None, from_date=None, to_date=None, limit=50, cursor=NoneLedgerPage
get_ledger_entryuser_id: str, entry_id: strLedgerEntry | None
list_usage_entriesuser_id: str, from_date=None, to_date=None, limit=50, cursor=NoneLedgerPage
list_usage_chargesuser_id: str, from_date=None, to_date=None, limit=50, cursor=NoneUsageChargePage

Analytics

Usage analytics aggregate across all users of the tenant within a [start, end) window. These are optional store capabilities — custom stores that omit them raise CapabilityNotSupportedError.

MethodParametersReturns
spend_by_userstart: datetime, end: datetimelist[SpendByUserRow]
spend_by_modelstart: datetime, end: datetimelist[SpendByModelRow]
top_userslimit: int, start: datetime, end: datetimelist[TopUserRow]
daily_spendstart: datetime, end: datetimelist[DailySpendRow]
aggregate_statsstart: datetime, end: datetimeAggregateStats

Teams

Team management is a store-level capability: create_team, get_team_balance, add_team_member, remove_team_member, and list_team_members live on the CreditStore (call them on your store object), while bursar.credits.deduct_team charges the shared pool through the facade.

store.create_team(team_id, display_name="Acme")
store.add_team_member(team_id, user_id)
bursar.credits.deduct_team(
team_id, user_id,
UsageMetrics(operation="execution", measures={"jobs": 1}, dimensions={"model": "gpt-4o"}),
idempotency_key="team:run:3",
)
MethodParametersReturns
deduct_teamteam_id: str, user_id: str, metrics: UsageMetrics, idempotency_key: str, metadata=NoneTeamDeductionResult

See Credit lifecycle for the full account model, and Leases and financial safety for lease guarantees.