03 - Plans and Allowances
Most SaaS products offer pricing tiers - Free, Pro, Enterprise - each with a free monthly allowance of credits. ducto tracks per-user plan assignments and monthly usage windows, automatically falling back to the user's credit balance when the free allowance is consumed.
Think of the allowance as a monthly prepaid bucket. Every Pro user gets 50,000 free credits each month. Every Free user gets 5,000. These buckets reset automatically at the start of each billing period. In contrast, the credit balance (managed via add_credits and deduct_credits from the previous notebook) is permanent - it only changes when credits are manually added or deducted.
Allowance tracking works through plan definitions embedded in the pricing configuration. Each plan specifies a free_allowance - the number of free credits per billing period. When a user is assigned to a plan, the system records the current billing period (a monthly window). Each time the user spends credits, the system first deducts from the free allowance. Once the allowance is exhausted, further spending draws from the user's purchased credit balance (pay-as-you-go).
In this notebook we will use MemoryStore because plan management through PostgresStore requires pre-seeded credit_plans table rows. MemoryStore handles plan definitions inline from PricingConfigData, making it the ideal choice for learning and experimentation.
import uuid
from datetime import datetime, timedelta
from ducto.interface.memory import MemoryStore
from ducto.manager import CreditManager
from ducto.engine import PricingEngine
from ducto.metrics import UsageMetrics, ToolCall
from ducto.interface.models import (
PricingConfigData, PlanDefinition,
CreditMetadata, SpendCap,
)
store = MemoryStore()
store.setup()
print("✔ MemoryStore ready.")
Persist plan definitions in pricing config
In ducto, plan definitions live inside the pricing configuration, right alongside the model pricing formulas. This keeps all pricing logic - both per-unit costs and subscription allowances - in a single place for easy maintenance.
Each plan has three key properties: an id (used to reference the plan when assigning users), a human-readable name, and a free_allowance (the number of free credits the user gets each billing period). The billing period is a monthly window. When a user is assigned to a plan, the system records the period_start and period_end. The allowance resets automatically when the period ends.
This auto-reset makes the allowance fundamentally different from the balance. The allowance refills every month, while the balance only changes when credits are manually added or deducted through add_credits and deduct_credits. A Pro user with 50,000 monthly allowance still needs a credit balance for usage beyond the free tier.
# Store pricing configuration with both model formulas and plan definitions.
# MemoryStore.set_active_pricing() extracts plan definitions from PricingConfigData.
# This keeps all pricing logic in one place for easy maintenance.
store.set_active_pricing(
PricingConfigData(
# Model pricing formulas — same format as PricingEngine.from_dict().
models={
"gpt-4o": "input_tokens * 5 + output_tokens * 15",
},
# Plan definitions — each plan specifies a free monthly allowance.
# "pro" tier: users get 50,000 free credits per month
# "free" tier: users get 5,000 free credits per month
plans={
"pro": PlanDefinition(
id="pro", name="Pro Tier",
free_allowance=50_000,
),
"free": PlanDefinition(
id="free", name="Free Tier",
free_allowance=5_000,
),
},
),
label="default",
)
print(" Pricing config stored with 2 plan definitions: Pro (50,000/mo) and Free (5,000/mo)")
Assign a user and check allowance
Once plans are configured, we can assign a user to a plan and check their remaining free allowance. The assignment is done via set_user_plan(), which links a user ID to a plan ID in the store.
The check_allowance() method returns an AllowanceResult that includes the plan ID, the current billing period's start and end dates, and the remaining allowance. Initially, a new Pro user has the full 50,000 allowance available - no credits have been consumed yet in the current billing period. The period dates tell you exactly when the allowance will reset.
# Generate a new user and assign them to the "pro" plan.
# set_user_plan() links the user ID to the plan definition stored earlier.
user = str(uuid.uuid4())
store.set_user_plan(user, "pro")
# Check how many free credits the user has remaining in this billing period.
# AllowanceResult contains:
# - plan_id: the name of the plan the user is on
# - period_start: the beginning of the current billing period (monthly window)
# - period_end: when the current period ends and the allowance resets
# - allowance_remaining: how many free credits are still available this period
allow = store.check_allowance(user)
print(f" Plan: {allow.plan_id}")
print(f" Period: {allow.period_start} → {allow.period_end}") # Monthly window
print(f" Remaining: {allow.allowance_remaining}") # Full 50,000 available since no usage yet
assert allow.allowance_remaining == 50_000
print(" ✓ Full 50 000 free allowance available")
Consume allowance
When a user makes a request that costs credits, the system should first consume the free allowance before drawing from the purchased balance. The increment_usage_window() method records usage against the user's allowance for the current billing period.
After calling increment_usage_window(), the next call to check_allowance() returns a reduced remaining amount. Once the allowance reaches zero, further requests use the user's purchased credit balance (pay-as-you-go). The allowance never goes negative - it stops at zero and the system switches to balance-based charging.
# Consume 3,000 credits from the user's free allowance.
# In production, this would be called alongside deduct_credits() to
# also track how much of the free allowance has been used this period.
store.increment_usage_window(user, "pro", 3_000)
# Check the allowance again to confirm it was reduced by the correct amount.
allow2 = store.check_allowance(user)
print(f" Remaining after 3 000 used: {allow2.allowance_remaining}")
assert allow2.allowance_remaining == 47_000 # 50,000 - 3,000
print(" ✓ Allowance correctly reduced")
Free tier vs Pro tier
Different plans have different allowance amounts. The Free tier typically offers a small monthly allowance to let users evaluate the product, while the Pro tier offers substantially more for regular active usage.
In our configuration, the Free tier has a 5,000-credit monthly allowance - one-tenth of the Pro tier's 50,000. This means a Free user would exhaust their allowance after roughly 1,000 gpt-4o input tokens, while a Pro user could run over 10,000 tokens before hitting the cap. Once the allowance runs out, both tiers continue to work, but they charge against the user's purchased credit balance instead.
# Create a Free tier user and compare their allowance to the Pro user.
free_user = str(uuid.uuid4())
store.set_user_plan(free_user, "free")
free_allow = store.check_allowance(free_user)
print(f" Free user allowance: {free_allow.allowance_remaining}") # 5,000 — ten times less than Pro
assert free_allow.allowance_remaining == 5_000
print(" ✓ Free tier gets 5 000/month")