Skip to navigation

Spend limits

Cap what a customer, team, member or agent can spend in a period, and let customers set their own.

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

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.

countsWhat counts toward the limit
paid_usage (default)Credits drawn from paid grants, valued at each grant’s cost_basis, and on a postpaid plan the confirmed and pending charges. Included and promotional credits count as zero, so a customer spending free credits isn’t stopped by a money limit
all_usageEverything, with credits that have no cost basis valued at the plan’s top_up.unit_amount_decimal. A plan with no top-up price values them at zero

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.

ConditionLimits
customerOne customer
planEvery customer on a plan
agent_idWork done by one of your agents
member_idOne person inside a customer
team_idOne team inside a customer
poolConsumption from one pool
noneThe whole account

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.

  1. 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.
  2. Across groups, the strictest wins. The work must have room in every group that applies. If the customer group has 500leftandtheteamgrouphas500 left and the team group has 20, the work is denied once it would pass $20.
  3. 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’s ceiling, or than its amount when it has no ceiling. A higher value returns parameter_invalid.

Create a limit

POST /v1/spend_limits
{
"key": "growth_team_monthly",
"applies_to": { "customer": "cus_9sF2kq81LmQz", "team_id": "team_growth" },
"period": { "duration": "P1M", "anchor": "billing_period" },
"amount": { "amount": 200000, "currency": "usd" },
"action": "block",
"alert_at": ["0.75", "0.9", "1"]
}
Response: SpendLimit
{
"id": "sl_4kP7wD2nRt",
"object": "spend_limit",
"key": "growth_team_monthly",
"applies_to": { "customer": "cus_9sF2kq81LmQz", "team_id": "team_growth" },
"period": { "duration": "P1M", "anchor": "billing_period" },
"amount": { "amount": 200000, "currency": "usd" },
"counts": "paid_usage",
"action": "block",
"alert_at": ["0.75", "0.9", "1"],
"set_by": "operator",
"enabled": true,
"current": {
"spent": { "amount": 0, "currency": "usd" },
"held": { "amount": 0, "currency": "usd" },
"period_start": "2026-11-01T00:00:00Z",
"resets_at": "2026-12-01T00:00:00Z"
},
"actor": { "type": "key", "id": "key_1xB6nW3dQz", "name": "Billing backend" },
"livemode": false,
"created_at": "2026-11-04T13:12:09Z"
}
  • Period. P1D, P1W or P1M. anchor is calendar (UTC midnight, Monday, the first of the month), billing_period (the customer’s subscription period) or customer (the moment the subscription was created, today the same as subscription_start).
  • Action. warn adds the warning spend_limit_exceeded and sends a webhook, and check() still allows the work. block also makes check() deny the work. The default is warn.
  • Amount. Its currency must be the customer’s currency, or the plan’s.
  • Changes. PATCH /v1/spend_limits/{id} changes amount, ceiling, counts, action, alert_at and enabled, and a field you leave out keeps its value. applies_to and period can’t change; delete the limit and create a new one. The limit’s current shows 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.

Response: CheckResult
{
"object": "check",
"allowed": false,
"reason": "spend_limit_reached",
"customer": "cus_9sF2kq81LmQz",
"credit_type": "ai_credits",
"required": "40",
"available": "1260",
"denied_by": {
"type": "spend_limit",
"id": "sl_4kP7wD2nRt",
"resets_at": "2026-12-01T00:00:00Z",
"member_id": "user_2231",
"remaining": "0"
},
"affordability": {
"balance": { "available_before": "1260", "available_after": "1220", "shortfall": "0" },
"pools": [],
"allowances": [],
"spend_limits": [
{
"id": "sl_4kP7wD2nRt",
"key": null,
"action": "block",
"counts": "paid_usage",
"currency": "usd",
"counted": "40",
"remaining_before": "0",
"remaining_after": "-40",
"shortfall": "40",
"resets_at": "2026-12-01T00:00:00Z"
}
],
"would_exceed": true,
"reason": "spend_limit_reached"
},
"livemode": false
}

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

LimitCreated byIn live mode
Account, plan, agent, member, team or poolYou, as configurationA config proposal an Admin approves. A live key gets 403 with proposal_required
One customer, including when a team or member is named tooYour backend, or the customer through the customer viewAllowed with a key that has spend_limits:write
A plan’s default (default_spend_limit)You, on the planPart of the plan, so it follows plan rules

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.

Config document: plans
{
"plans": [
{
"key": "pro_monthly",
"name": "Pro",
"billing_mode": "prepaid",
"currency": "usd",
"interval": "month",
"credit_type": "ai_credits",
"recurring_fee": { "amount": 4900, "currency": "usd" },
"included_credits": { "amount": "5000" },
"top_up": { "unit_amount_decimal": "1", "minimum_credits": "1000", "maximum_credits": "1000000" },
"default_spend_limit": {
"amount": { "amount": 50000, "currency": "usd" },
"ceiling": { "amount": 500000, "currency": "usd" },
"action": "block",
"alert_at": ["0.75", "0.9", "1"]
},
"rates": [{ "event_name": "agent.task", "credits_per_unit": "10" }]
}
]
}

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