Skip to main content

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

RuleTypeDefaultDescription
enabledbooleantrueToggle enforcement on/off
action_on_exceedstringblockAction when over cap: block, warn, throttle
warning_threshold_percentnumber80Emit 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:

  1. A sub-user identity is present on the context (context.sub_user_identity or context.user_id)
  2. The matching WaxellUser row has a non-null, non-zero monthly_budget_cap_cents

Tenants that don't set per-user caps see no policy effect.

Context Attributes Read

AttributePhasePurpose
context.sub_user_identityallResolve sub_user_id for the active end-user
context.user_idallFallback resolver when sub-user identity isn't set
context.metadata["tenant_id"] / context.tenant_idallScope 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

FieldExample (BLOCK)
Categoryend-user-budget
Actionblock
ReasonEnd-user 'emp-4127' monthly budget exceeded ($201.42 / $200.00).
Metadata{"sub_user_id": "emp-4127", "spent_cents": 20142, "cap_cents": 20000}
FieldExample (WARN)
Actionwarn
ReasonEnd-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_cents is 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 $200 cap is 20000, not 200.
  • Observe-plane uses Django ORM -- runtime plane wires a Redis-backed aggregator via the _usage_query_fn seam for low-latency reads.

Next Steps