End-User Budget Policy
The end-user-budget policy category enforces per-end-user monthly spend caps. Each WaxellUser row can carry a monthly_budget_cap_cents value; this handler sums that user's WaxellUserActivity.cost_cents for the current calendar month and blocks (or warns / throttles) when the cap is hit.
Use it when a tenant has many sub-users (employees, customers, agent-of-agent identities) and you want per-seat cost ceilings, not just a tenant-wide spend cap. The tenant-wide cap is handled by the Budget Policy.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Toggle enforcement on/off |
action_on_exceed | string | block | Action when over cap: block, warn, throttle |
warning_threshold_percent | number | 80 | Emit WARN when spend reaches this percent of cap |
How It Works
The end-user-budget handler runs at before_workflow, mid_execution, and before_domain_call. after_workflow is a no-op (cap is enforced before spend, not after).
The handler only fires when:
- A sub-user identity is present on the context (
context.sub_user_identityorcontext.user_id) - The matching
WaxellUserrow has a non-null, non-zeromonthly_budget_cap_cents
Tenants that don't set per-user caps see no policy effect.
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.sub_user_identity | all | Resolve sub_user_id for the active end-user |
context.user_id | all | Fallback resolver when sub-user identity isn't set |
context.metadata["tenant_id"] / context.tenant_id | all | Scope the user lookup |
Data Sources
WaxellUser.monthly_budget_cap_cents-- the per-user cap (integer cents)WaxellUserActivity.cost_cents-- summed over the current calendar month
Runtime plane can inject _usage_query_fn to route through a Redis-backed aggregator instead of Django ORM; observe plane uses ORM by default.
Example Policy
{
"name": "Per-seat $200/mo Cap",
"category": "end-user-budget",
"rules": {
"enabled": true,
"action_on_exceed": "block",
"warning_threshold_percent": 75
},
"scope": {
"agents": ["*"]
},
"enabled": true
}
SDK Integration
Pass the end-user identity via user_id / user_group on the decorator. The handler reads it off the context and looks up the matching WaxellUser row.
import waxell_observe as waxell
waxell.init()
@waxell.observe(
agent_name="research-assistant",
user_id="emp-4127",
user_group="enterprise",
enforce_policy=True,
)
async def run_research(query: str) -> str:
return await do_research(query)
Set the cap by updating the WaxellUser row:
wax end-users update emp-4127 --monthly-cap-cents 20000 # $200
Observability
| Field | Example (BLOCK) |
|---|---|
| Category | end-user-budget |
| Action | block |
| Reason | End-user 'emp-4127' monthly budget exceeded ($201.42 / $200.00). |
| Metadata | {"sub_user_id": "emp-4127", "spent_cents": 20142, "cap_cents": 20000} |
| Field | Example (WARN) |
|---|---|
| Action | warn |
| Reason | End-user 'emp-4127' approaching budget (82% used). |
| Metadata | {"percent_used": 82.1, ...} |
Common Gotchas
- Cap is per-user, not per-tenant. Tenant-wide ceilings still need a Budget Policy.
- No sub-user on context = ALLOW. This category is a no-op for runs that don't carry end-user identity. Pair with a tenant-level Budget Policy for those.
monthly_budget_cap_centsis null by default. No cap row = no enforcement. The handler doesn't synthesize a default cap.- Calendar month, not rolling 30 days. The window starts at midnight on the 1st. Spend resets at month boundary.
- Spend is in cents. A
$200cap is20000, not200. - Observe-plane uses Django ORM -- runtime plane wires a Redis-backed aggregator via the
_usage_query_fnseam for low-latency reads.
Next Steps
- Budget Policy -- Tenant-wide monthly spend cap
- End-User Rate Limit -- Per-user request-rate cap
- End-User Suspension -- Block runs for suspended end-users
- Policy Categories & Templates -- All categories
- User Tracking -- How sub-user identity flows through Observe