Skip to main content

Budgets and spend limits

Usage-based pricing worries customers: what if a script runs all weekend? A spend limit they control fixes that. This one is a monthly overage budget in euros, set from your app's billing settings, with a warning at 80%.

import { Limitr } from '@formata/limitr';

const limitr = await Limitr.new(`
policy: {
exchange: { euro: { value: 1.14, currency: 'usd' } }
credits: {
ai_token: { label: 'AI token', price: { amount: 0.00002 }, overhead_cost: 0.000008 }
}
plans: {
pro: {
default: true
entitlements: {
// 1M tokens included each month, then billed.
chat: {
limit: {
credit: 'ai_token', mode: 'soft', value: 1_000_000
resets: true, reset_sch: 'monthly:1'
}
}
}
}
}
}`);

limitr.addHandler('budget', (name, value) => {
const { customer, cap } = JSON.parse(value as string);
if (name === 'cap-threshold-crossed' && cap.id === 'overage_warning') {
console.log(` email ${customer.id}: 80% of your overage budget is used`);
}
if (name === 'cap-limit') {
console.log(` denied: ${customer.id} reached its €${cap.value} budget`);
}
});

await limitr.ensureCustomer('ada');

// The customer sets a monthly overage budget of €10 in your app's billing settings.
async function setBudget(customer: string, euros: number) {
await limitr.removeCustomerCap(customer, 'overage_budget', false);
await limitr.removeCustomerCap(customer, 'overage_warning', false);
// Counted in euros, only on overage, starting over each month.
const monthly = {
credit: 'euro',
overage_only: true,
resets: true, reset_sch: 'monthly:1',
};

await limitr.addCustomerCap(customer, euros, {
...monthly,
cap_id: 'overage_budget',
});
await limitr.addCustomerCap(customer, euros * 0.8, {
...monthly,
cap_id: 'overage_warning',
observe_only: true, // warns, never denies
});
}
await setBudget('ada', 10);

// Usage inside the plan never counts against an overage budget.
console.log('1M included tokens:', await limitr.allow('ada', 'chat', 1_000_000));
// 100k tokens at $20 per million, in euros.
const euros = (100_000 * 0.00002 / 1.14).toFixed(2);
for (let i = 1; i <= 6; i++) {
console.log(`100k more tokens (€${euros}):`, await limitr.allow('ada', 'chat', 100_000));
}

const spent = (await limitr.customerCap('ada', 'overage_budget'))!.meter_value;
console.log(`overage this month: €${spent.toFixed(2)} of €10`);

It prints:

1M included tokens: true
100k more tokens (€1.75): true
100k more tokens (€1.75): true
100k more tokens (€1.75): true
100k more tokens (€1.75): true
email ada: 80% of your overage budget is used
100k more tokens (€1.75): true
denied: ada reached its €10 budget
100k more tokens (€1.75): false
overage this month: €8.77 of €10

How it works​

  • A spend cap is a ceiling across calls. This one counts in euros (through the policy's exchange), counts only overage (overage_only), and starts over each month.
  • The warning is a second cap at 80% with observe_only: it never denies, and sends cap-threshold-crossed once when it's reached.
  • Denials send cap-limit with the cap that stopped the call, so your app can say why.
  • Caps only restrict. They never allow something a limit would deny.

Changing a budget​

A cap's value can't be edited in place. Remove it and add a new one, as setBudget does. The new cap starts counting from zero, so change budgets at the start of a period, or add what was already spent to the new value.

Budgets for an org and its teams​

A cap with shared: true applies to every customer whose refs lead to the holder:

const monthly = { shared: true, resets: true, reset_sch: 'monthly:1' };

await limitr.addCustomerCap('acme', 500, { ...monthly, cap_id: 'org_monthly' });
await limitr.addCustomerCap('team_a', 100, { ...monthly, cap_id: 'team_monthly' });

A user in team A is stopped by whichever budget runs out first. shared_with: ['user'] limits a shared cap to calls made by users.

Other budgets​

BudgetCap
$50 a month across everythingaddCustomerCap(id, 50, { resets: true, reset_sch: 'monthly:1' })
Only some features{ scope: ['chat', 'images'] }
What a customer can cost you{ overhead_cost: true }
Usage covered by credit packs doesn't count{ ignore_grants: true }
A one-off budget for a jobPass a cap with the call (Stof), or add one and remove it after.
Tokens instead of money{ credit: 'ai_token' }

See Spend caps for every option.