Spend limits
A spend limit caps the money value of the work a scope consumes in a period. It answers “how much is this team allowed to spend this month?”, which a credit balance can’t, because a balance says what a customer has and not what they are willing to use.
Before you start
- You call
check()andtrack()for each piece of work. - For limits by member, team or agent, you send
member_id,team_idoragent_idon those calls.
With no spend limit there is no cap. Limits sit beside the balance. check() tests the balance and every limit that applies.
What a limit counts
A limit counts the money value of consumption, not credits. That keeps one limit comparable across plans, pools and burn multipliers.
Buying credits never counts. Only consumption does. Active holds count as spent until they are captured or released, so two agents can’t both reserve the last of a budget.
Where a limit applies
applies_to holds the conditions a limit needs. Every condition you send must match.
agent_id, member_id and team_id are the values you send on check(), track() and trace(). Runledger doesn’t model members or teams; they are labels you choose. See Members, teams and agents.
Which limit wins
Limits are grouped by which of agent_id, member_id, team_id and pool they name. A limit that names none of them is in the customer group.
- Inside a group, the most specific limit applies. For a customer that is a limit on the customer, then one on the customer’s plan, then one on the account.
- Across groups, the strictest wins. The work must have room in every group that applies. If the customer group has 20, the work is denied once it would pass $20.
- A customer can only tighten. A limit your customer sets (
set_by: customer) replaces the operator’s limit in its group, and it can’t be higher than that limit’sceiling, or than itsamountwhen it has no ceiling. A higher value returnsparameter_invalid.
Create a limit
- Period.
P1D,P1WorP1M.anchoriscalendar(UTC midnight, Monday, the first of the month),billing_period(the customer’s subscription period) orcustomer(the moment the subscription was created, today the same assubscription_start). - Action.
warnadds the warningspend_limit_exceededand sends a webhook, andcheck()still allows the work.blockalso makescheck()deny the work. The default iswarn. - Amount. Its currency must be the customer’s currency, or the plan’s.
- Changes.
PATCH /v1/spend_limits/{id}changesamount,ceiling,counts,action,alert_atandenabled, and a field you leave out keeps its value.applies_toandperiodcan’t change; delete the limit and create a new one. The limit’scurrentshows what has been spent this period.
GET /v1/spend_limits lists limits, newest first, and filters by customer, plan, agent_id, member_id, team_id, set_by and enabled.
What a denial looks like
A block limit denies in check() with the reason spend_limit_reached. denied_by names the limit and when it lifts, so your product can tell the person what to do.
A spend limit with action: warn is listed in affordability.spend_limits the same way, with its remaining_before, remaining_after and shortfall, but it doesn’t set would_exceed, because the work is allowed. would_exceed is true only when something would block: a balance shortfall, an allowance that stops at its cap, or a spend limit with action: block. See Checks and holds.
track() never blocks, because the work already happened. If it takes a limit past its amount, the event is recorded and carries the warning spend_limit_exceeded.
Alerts
alert_at lists decimals from 0 to 1. The first time spend reaches each one in a period, Runledger sends spend_limit.threshold_reached, once per period per threshold. A threshold of "1" fires when the limit is full. The event’s context says which limit, which member_id and team_id, and the threshold. See Auto top-up and alerts.
Who can change what
A limit for one customer is data, like a credit grant. It isn’t part of your configuration, and a customer changing their own limit is never held for approval.
In live mode an agent key can create a customer limit and tighten one, and can’t loosen or delete one: a higher amount or ceiling, counts narrowed from all_usage to paid_usage, action from block to warn, an alert_at threshold raised or dropped, or enabled: false is refused, and so is a customer-set limit it creates that is looser in any of those than the limit it replaces. That refusal is permission_denied, and it applies only to agent keys. This keeps an AI agent that is working for your customer from loosening the guard rails around itself.
Operator limits belong in the configuration document under spend_limits, matched by key, so they go through dry run, the sandbox and a live proposal like everything else.
Plan defaults
A plan’s default_spend_limit gives each new subscription a limit that your customer can replace with their own, up to ceiling.
default_spend_limit can change at any time and applies to subscriptions created afterwards. Existing subscriptions keep the limit they have.
Record keeping
Every create, change and delete is an audit event with the actor, and every limit shows who last changed it. A customer who sets a limit on the customer view is recorded as actor system named customer_session, with the session’s ID. Spend counters are derived from the ledger, so they never drift from it.
Sandbox
Create limits with a sandbox key and use a test clock to cross a period boundary. The spend_limit scenario in the MCP server’s run_scenario tool spends against a limit until a block denies the work.
API reference
Create, list, retrieve, update and delete a spend limit.
Next steps
- Members, teams and agents shows how usage is attributed.
- Auto top-up and alerts covers the alerts a limit can send.
- Customer view lets customers set their own limits.
- Going live lists what to check before real billing starts.