Skip to main content

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.

OptionDefaultNotes
cap_idgeneratedThe 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').
exchangeabletrue for runeConvert other credits into the cap's credit. False: only usage in exactly that credit counts. A named credit defaults to false.
scopeallEntitlement names the cap applies to (Ex. ['chat', 'images']).
overage_onlyfalseOnly usage past included plan limits counts. "A ceiling on what we bill them."
ignore_grantsfalseUsage covered by grants doesn't count.
overhead_costfalseCount what calls cost you instead of what they're worth. "A ceiling on what they cost us."
observe_onlyfalseNever deny. Count, and send cap-threshold-crossed when value is reached.
hard_trailingfalseLet through the call that crosses the ceiling (it was under before), deny the calls after.
follow_decrementsfalseDecrements lower the cap's count. Off by default: caps count spend one way.
sharedfalseAlso applies to every customer whose refs lead to this one (an org budget).
shared_withall typesWith shared: only these caller types (Ex. ['user']).
resets, reset_inc, reset_schStart the count over each period, as on a limit.
expires_onnullRemove the cap after this time (ms).
send_eventstrueSend 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_only counts only usage past the limit, and overhead_cost counts 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 (and settle) record usage that already happened, even past a cap.

Common caps​

BudgetCap
$500 a month across everything, for the whole orgaddCustomerCap(org, 500, { shared: true, resets: true, reset_sch: 'monthly:1' })
At most €15 of overage a monthaddCustomerCap(id, 15, { credit: 'euro', overage_only: true, resets: true, reset_sch: 'monthly:1' })
A customer can't cost us more than $20 a monthaddCustomerCap(id, 20, { overhead_cost: true, resets: true, reset_sch: 'monthly:1' })
Warn at $100, never blockaddCustomerCap(id, 100, { observe_only: true })
2M tokens a day, across chat and codeaddCustomerCap(id, 2_000_000, { credit: 'ai_token', scope: ['chat', 'code'], resets: true, reset_inc: 86_400_000 })
A team budget under the org'sa 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​

CallDoes
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​

EventWhen
cap-limitA cap denied a call. Carries the cap, the customer holding it, the caller, and the call's meter values.
cap-threshold-crossedA cap's count reached its value, once. The event to warn or upsell from.
customer-cap-added / -reset / -removedCap changes.

When a call moves a cap held by another customer (an org's shared cap), that customer also gets customer-set.