CreditManager — Python
Orchestrates the full credit lifecycle: calculate → reserve → deduct.
Constructor
from ducto import CreditManager
from ducto.interface.memory import MemoryStore
store = MemoryStore()
manager = CreditManager(store=store)
Optionally pass a pre-configured PricingEngine and/or CreditEventEmitter:
from ducto.events import CreditEventEmitter
emitter = CreditEventEmitter()
manager = CreditManager(store=store, engine=engine, emitter=emitter)
Pricing
publish_pricing_from_dict(data)
manager.publish_pricing_from_dict({
"version": 1,
"models": {"_default": "input_tokens * (0.001 / 1000)"},
})
load_pricing_from_store()
Fetch active pricing from the database store.
publish_pricing(config, label=None)
Load from a PricingConfigData object.
Balance Operations
add_credits(user_id, amount, type="adjustment", metadata=None, expires_at=None)
Add credits. Optional expires_at for time-bound credits.
get_balance(user_id)
reserve_credits(user_id, amount, op_type="usage", metadata=None, min_balance=None)
deduct(user_id, metrics, idempotency_key=None, metadata=None)
Full lifecycle: calculate cost → consume plan allowance → check spend caps → reserve → deduct.
deduct_fixed(user_id, job_name, idempotency_key=None, metadata=None)
Deduct a fixed cost (configured in pricing fixed section).
Plan Management
Plans provide free monthly allowances consumed before balance deductions.
store.set_user_plan(user_id, plan_id)
Assign a plan to a user.
store.get_user_plan(user_id)
Get user's current plan details.
store.check_allowance(user_id)
Get remaining free allowance for the current billing period.
Example — full plan coverage skips balance deduction:
config = PricingConfigData(
models={"_default": "input_tokens * 1"},
plans={"free": PlanDefinition(id="free", name="Free", free_allowance=100)},
)
store.set_active_pricing(config)
store.set_user_plan("user_1", "free")
store.add_credits("user_1", 10)
result = manager.deduct("user_1", UsageMetrics(input_tokens=5))
assert result.amount == 0 # fully covered by allowance
assert result.balance_after == 10 # balance unchanged
Refunds
refund_credits(transaction_id, amount=None, reason=None, metadata=None)
Refund a previous deduction. Supports partial refunds. Returns error for duplicates.
result = manager.refund_credits(tx_id)
assert result.amount == 1
assert result.new_balance == 100
Usage Analytics
Time-windowed aggregation queries. All accept start / end datetime range.
| Method | Returns |
|---|---|
spend_by_user(start, end) | list[SpendByUserRow] — per-user totals |
spend_by_model(start, end) | list[SpendByModelRow] — per-model spend |
top_users(limit, start, end) | list[TopUserRow] — sorted by spend desc |
daily_spend(start, end) | list[DailySpendRow] — bucketed by day |
aggregate_stats(start, end) | AggregateStatsRow — total, active users, avg daily, top model, top user |
Credit Expiry
sweep_expired_credits(dry_run=False)
Sweep expired credits from all users' balances. Dry-run reports without modifying.
manager.add_credits("user_1", 100, "purchase", expires_at=datetime(2024, 1, 1))
result = manager.sweep_expired_credits()
assert result.expired_count == 1
assert result.expired_amount == 100
dry_run = manager.sweep_expired_credits(dry_run=True)
assert dry_run.dry_run is True # balance unchanged
Spend Caps
Per-user daily/monthly spend limits configured at the store level.
from ducto.interface.models import SpendCap
store.set_spend_cap(SpendCap(
user_id="user-1",
cap_type="daily",
limit=50,
action="deny", # "deny" | "warn" | "notify"
model="gpt-4", # optional per-model cap
))
- deny — raises
InsufficientCreditsErrorwhen exceeded - warn — emits
credits.cap_warningevent but allows deduction - notify — emits
credits.cap_warningevent
Team / Shared Balances
Teams have their own credit pool separate from individual user balances.
| Method | Description |
|---|---|
store.create_team(name, initial_balance=0) | Create team with shared pool |
store.get_team_balance(team_id) | Fetch team balance + member count |
store.add_team_member(team_id, user_id, role="member", spend_cap=None) | Add member with optional per-user cap |
store.get_team_members(team_id) | List team members with spend + caps |
store.deduct_team(team_id, user_id, amount, metadata=None) | Deduct from team pool |
manager.deduct_team(team_id, user_id, metrics, metadata=None) | Pricing-engine cost + team deduction |
team = store.create_team("Engineering", 1000)
store.add_team_member(team.team_id, "user-1", role="admin", spend_cap=500)
result = manager.deduct_team(team.team_id, "user-1", UsageMetrics(input_tokens=100))
assert result.amount == -100
assert result.team_balance_after == 900
Events
Optional pub/sub event system for credit lifecycle events.
from ducto.events import CreditEvent, CreditEventEmitter
emitter = CreditEventEmitter()
manager = CreditManager(store=store, emitter=emitter)
@emitter.on("credits.deducted")
def handle_deduct(event: CreditEvent):
print(f"{event.user_id} spent {event.data['amount']} credits")
# Available event types:
# "credits.deducted" | "credits.added" | "credits.refunded"
# | "credits.expired" | "credits.cap_reached"
# | "credits.cap_warning" | "credits.low_balance" | "credits.plan_changed"
Properties
| Property | Type | Description |
|---|---|---|
engine | PricingEngine | None | Internal engine (None until loaded) |