Skip to main content
Version: 2.x

:::info Executable tutorial

This page is generated from a tested Jupyter notebook. Open it in Google Colab or view the source notebook.

:::

Share credits across a team

A Bursar team owns a shared credit pool while each member retains an independently enforced spend cap. This tutorial creates a team, manages membership, charges the shared pool, and verifies that membership changes do not reset spend history.

Learning objectives​

After completing this tutorial, you can:

  • Create and fund a team account
  • Add members with individual spend caps
  • Charge usage against the shared pool
  • Remove and restore membership without losing audit history

Prerequisites​

  • Complete the credit lifecycle tutorial
  • Start the notebook server from samples/python/notebooks/

Setup​

Each notebook starts a throwaway Postgres cluster, runs the bursar schema, and publishes the demo configuration. publish_config returns a Bursar facade bound to the store; team operations live on the store and on bursar.credits.

import atexit
from shared import start_postgres_store, cleanup, base_config, publish_config, USER_ADA, USER_ALEX, USER_JAMAL
from decimal import Decimal
from bursar.metrics import UsageMetrics

store, pgdata = start_postgres_store()
atexit.register(cleanup, pgdata)
bursar = publish_config(store, base_config())
credits = bursar.credits
print("config published")

Creating a team​

create_team(owner_subject_id, name, initial_balance) provisions the pool and makes the owner its first member with an uncapped role. get_team_balance reports the pool balance and the number of members.

team = store.create_team(
owner_subject_id=USER_ADA,
name="Acme",
initial_balance=Decimal("1000"),
idempotency_key="team:acme:create",
)
print("team_id:", team.team_id)

balance = store.get_team_balance(team.team_id)
print("balance:", balance.balance)
print("members:", balance.member_count)

Adding members with spend caps​

add_team_member(team_id, subject_id, role, spend_cap) adds a member with a cap measured in credits. get_team_members returns each member's role, cap, and cumulative spend.

store.add_team_member(team.team_id, USER_ALEX, role="member", spend_cap=Decimal("200"))
store.add_team_member(team.team_id, USER_JAMAL, role="admin", spend_cap=Decimal("300"))

for member in store.get_team_members(team.team_id):
print(member.user_id[:8], member.role, "cap:", member.spend_cap, "spent:", member.total_spent)

Spending against the team pool​

credits.deduct_team(team_id, user_id, metrics, idempotency_key=...) prices the usage with the member's plan rate card, debits the team pool (not the member's personal balance), and attributes the charge to the member. The caller-stable key makes retries replay-safe. The result carries the pool balance after the charge.

bursar.accounts.on_account_created(USER_ALEX, "acct_alex")

result = credits.deduct_team(
team.team_id,
USER_ALEX,
UsageMetrics(
operation="completion",
measures={"input_tokens": Decimal(40000), "output_tokens": Decimal(10000)},
dimensions={"model": "gpt-4o"},
),
idempotency_key="team:alex:completion:1",
)
print("charged:", result.amount)
print("team balance after:", result.team_balance_after)
print("entry_id:", result.entry_id)

for member in store.get_team_members(team.team_id):
print(member.user_id[:8], "spent:", member.total_spent)

Enforcing a member spend cap​

A member whose cumulative spend plus the requested charge would exceed their cap is rejected with CapReachedError; the team pool is untouched.

try:
credits.deduct_team(
team.team_id,
USER_ALEX,
UsageMetrics(
operation="completion",
measures={"input_tokens": Decimal(1000000), "output_tokens": Decimal(100000)},
dimensions={"model": "gpt-4o"},
),
idempotency_key="team:alex:cap-check:1",
)
except Exception as exc:
print(type(exc).__name__, str(exc)[:80])

print("balance unchanged:", store.get_team_balance(team.team_id).balance)

Membership lifecycle​

remove_team_member revokes access; the member's historical spend is preserved, so re-adding them with a new cap starts from their existing cumulative spend. The final cell always stops the cluster.

print("removed:", store.remove_team_member(team.team_id, USER_JAMAL))
store.add_team_member(team.team_id, USER_JAMAL, role="member", spend_cap=Decimal("500"))
print("members after re-add:", len(store.get_team_members(team.team_id)))
print("all done")