Spend caps
A spend cap is a budget: "no more than $50 a month across everything", "no more than €15 of overage", "this customer can't cost us more than $20". Limits are per entitlement; caps work across calls, entitlements, and credits.
Caps only restrict. A call has to pass its limit and every cap that applies. A cap never allows something the limit would deny.
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
policy: {
credits: {
ai_token: { label: 'AI token', price: { amount: 0.00002 }, overhead_cost: 0.000008 }
}
plans: {
team: {
default: true
entitlements: {
// every token is billed
chat: { limit: { credit: 'ai_token', mode: 'soft', value: 0 } }
}
}
}
}`);
limitr.addHandler('caps', (name, value) => {
const { cap, customer } = JSON.parse(value as string);
if (name === 'cap-limit') console.log(` denied by ${cap.id} (held by ${customer.id})`);
if (name === 'cap-threshold-crossed') console.log(` ${cap.id} crossed $${cap.value}`);
});
await limitr.ensureCustomer('acme', 'team', 'org');
await limitr.ensureCustomer('ada', '', 'user', 'Ada', ['acme']);
await limitr.ensureCustomer('bo', '', 'user', 'Bo', ['acme']);
// The org's budget: $5 a month, shared by everyone in it.
await limitr.addCustomerCap('acme', 5, {
cap_id: 'org_budget',
shared: true,
resets: true, reset_sch: 'monthly:1',
});
// Ada's own budget: $3. She's stopped by whichever runs out first.
await limitr.addCustomerCap('ada', 3, { cap_id: 'ada_budget' });
// A warning at $4 that never denies anything.
await limitr.addCustomerCap('acme', 4, {
cap_id: 'org_warning',
shared: true,
observe_only: true,
});
const calls = [
['ada', 100_000], ['ada', 100_000],
['bo', 100_000], ['bo', 100_000], ['bo', 50_000],
] as const;
for (const [who, tokens] of calls) {
const dollars = tokens * 0.00002;
const allowed = await limitr.allow(who, 'chat', tokens);
console.log(`${who}, ${tokens} tokens ($${dollars}):`, allowed);
}
const budget = await limitr.customerCap('acme', 'org_budget');
console.log('org spend so far: $' + budget?.meter_value);
It prints:
ada, 100000 tokens ($2): true
denied by ada_budget (held by ada)
ada, 100000 tokens ($2): false
org_warning crossed $4
bo, 100000 tokens ($2): true
denied by org_budget (held by acme)
bo, 100000 tokens ($2): false
org_budget crossed $5
bo, 50000 tokens ($1): true
org spend so far: $5
Adding caps
await limitr.addCustomerCap(customerId, value, options);
value is the ceiling, in the cap's credit. It returns the cap, or null when a cap with that cap_id already exists
on the customer (reset or remove it first) or the credit is unknown.
| Option | Default | Notes |
|---|---|---|
cap_id | generated | The cap's ID on the customer. Set one so you can find it again. |
credit | 'rune' | What the ceiling is in: 'rune' (dollars), a currency from the exchange ('euro'), or a credit ('ai_token'). |
exchangeable | true for rune | Convert other credits into the cap's credit. False: only usage in exactly that credit counts. A named credit defaults to false. |
scope | all | Entitlement names the cap applies to (Ex. ['chat', 'images']). |
overage_only | false | Only usage past included plan limits counts. "A ceiling on what we bill them." |
ignore_grants | false | Usage covered by grants doesn't count. |
overhead_cost | false | Count what calls cost you instead of what they're worth. "A ceiling on what they cost us." |
observe_only | false | Never deny. Count, and send cap-threshold-crossed when value is reached. |
hard_trailing | false | Let through the call that crosses the ceiling (it was under before), deny the calls after. |
follow_decrements | false | Decrements lower the cap's count. Off by default: caps count spend one way. |
shared | false | Also applies to every customer whose refs lead to this one (an org budget). |
shared_with | all types | With shared: only these caller types (Ex. ['user']). |
resets, reset_inc, reset_sch | Start the count over each period, as on a limit. | |
expires_on | null | Remove the cap after this time (ms). |
send_events | true | Send cap events. |
From Stof: add_customer_cap(customer_id, value, id = '', credit = 'rune', ...) with the same options as named
arguments, Ex. policy.add_customer_cap('acme', 500, id = 'monthly', shared = true, resets = true).
How caps count
- A cap counts the usage it applies to at the credit's price, converted to the cap's credit, including usage inside
the plan's allowance.
overage_onlycounts only usage past the limit, andoverhead_costcounts cost instead. - Every cap that applies must have room: the caller's own caps, shared caps on customers up its refs, and caps
passed with the call. A call that would go past any of them is denied and sends
cap-limit. - A call that lands exactly on the ceiling is allowed.
- Caps are checked before anything changes, so a denied call leaves no trace.
- Holds count. Room held by reservations counts against caps too.
force(andsettle) record usage that already happened, even past a cap.
Common caps
| Budget | Cap |
|---|---|
| $500 a month across everything, for the whole org | addCustomerCap(org, 500, { shared: true, resets: true, reset_sch: 'monthly:1' }) |
| At most €15 of overage a month | addCustomerCap(id, 15, { credit: 'euro', overage_only: true, resets: true, reset_sch: 'monthly:1' }) |
| A customer can't cost us more than $20 a month | addCustomerCap(id, 20, { overhead_cost: true, resets: true, reset_sch: 'monthly:1' }) |
| Warn at $100, never block | addCustomerCap(id, 100, { observe_only: true }) |
| 2M tokens a day, across chat and code | addCustomerCap(id, 2_000_000, { credit: 'ai_token', scope: ['chat', 'code'], resets: true, reset_inc: 86_400_000 }) |
| A team budget under the org's | a shared cap on the team; users are stopped by whichever runs out first |
Caps for one call
In Stof, allow and check take a cap for that call only: a cap, or a map of credit to ceiling.
// At most 50 cents for this call, on top of the customer's own caps.
policy.allow('ada', 'chat', 20_000, cap = map(('rune', 0.5)));
Managing caps
| Call | Does |
|---|---|
customerCap(id, capId) | The cap. meter_value is the count so far. |
resetCustomerCap(id, capId) | Start the count over (and its period). Sends customer-cap-reset. |
removeCustomerCap(id, capId) | Remove it. Sends customer-cap-removed. |
Caps live on the customer record (caps), so they're saved with it.
Events
| Event | When |
|---|---|
cap-limit | A cap denied a call. Carries the cap, the customer holding it, the caller, and the call's meter values. |
cap-threshold-crossed | A cap's count reached its value, once. The event to warn or upsell from. |
customer-cap-added / -reset / -removed | Cap changes. |
When a call moves a cap held by another customer (an org's shared cap), that customer also gets customer-set.