Bill from events
Limitr doesn't charge cards. It decides what's billable, prices it, and tells you as it happens. Your app records it and hands it to your payment provider (or Limitr Cloud does the ledger and invoices).
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
policy: {
credits: {
pro_month: { label: 'Pro plan', price: { amount: 49 } }
ai_token: { label: 'AI tokens', price: { amount: 0.00002 } }
seat: { label: 'Extra seats', price: { amount: 12 } }
}
plans: {
pro: {
default: true
entitlements: {
subscription: {
limit: {
credit: 'pro_month', mode: 'soft', value: 0
resets: true, reset_sch: 'monthly:1'
}
}
chat: {
limit: {
credit: 'ai_token', mode: 'soft', value: 1_000_000
resets: true, reset_sch: 'monthly:1'
}
}
seats: { scope: 'org', limit: { credit: 'seat', mode: 'soft', value: 3 } }
}
topups: {
tokens_500k: { credit: 'ai_token', value: 500_000, price: { amount: 8 } }
}
}
}
}`);
// Billable lines, written from events.
// In production: your database, then your payment provider.
type Line = { customer: string; item: string; quantity: number; amount: number };
const ledger: Line[] = [];
limitr.addHandler('billing', (name, value) => {
const event = JSON.parse(value as string);
if (name === 'meter-overage') {
const customer = event.customer.id;
const item = event.credit.label;
ledger.push({ customer, item, quantity: event.overage, amount: event.overage_price });
}
if (name === 'customer-topup-granted') {
const customer = event.customer.id;
const item = `Pack: ${event.topup_name}`;
ledger.push({ customer, item, quantity: 1, amount: event.topup.price.amount });
}
});
await limitr.ensureCustomer('acme', 'pro', 'org');
await limitr.ensureCustomer('ada', '', 'user', 'Ada', ['acme']);
await limitr.ensureCustomerPlanQuantity('acme'); // the plan fee, once a month
for (let i = 0; i < 4; i++) {
await limitr.increment('ada', 'seats'); // 4 seats, 3 included
}
await limitr.allow('acme', 'chat', 1_200_000); // 200k past the allowance
await limitr.applyCustomerTopup('acme', 'tokens_500k');
await limitr.allow('acme', 'chat', 600_000); // 500k from the pack, 100k billed
// The invoice.
let total = 0;
for (const line of ledger) {
const item = line.item.padEnd(18);
const quantity = String(line.quantity).padStart(7);
console.log(`${item} ${quantity} $${line.amount.toFixed(2)}`);
total += line.amount;
}
console.log(`${'Total'.padEnd(26)} $${total.toFixed(2)}`);
It prints:
Pro plan 1 $49.00
Extra seats 1 $12.00
AI tokens 200000 $4.00
Pack: tokens_500k 1 $8.00
AI tokens 100000 $2.00
Total $75.00
What to bill from
| Charge | Event | Amount |
|---|---|---|
| Usage past an allowance | meter-overage | overage_price: what grants didn't cover, priced by the credit's pricing model. |
Pure usage (value: 0) | meter-overage | The same: every unit is past the limit. |
| Extra seats | meter-overage on the seats entitlement | When a seat is added. For a monthly seat fee, bill the seats past the included amount each period. |
| The plan fee | meter-overage on the subscription entitlement | From ensureCustomerPlanQuantity, once a period. See Plan fees. |
| Credit packs | customer-topup-granted | The topup's price. Apply the topup after the payment succeeds. |
Don't bill meter-changed. It fires for usage inside the plan's allowance and for usage grants covered.
Getting it right
- Grants come off first. When a pack or included credits cover part of a call,
meter-overagereports what's left (overage,overage_price) and what was covered (grant_value_applied). When they cover all of it, there's nometer-overageat all. - Prices follow the tiers.
overage_priceis priced from where the customer already is this period, so tiered pricing bills each call correctly. Forvolumepricing, bill the period's total instead (see Pricing models). - Amounts are in runes, which is US dollars unless your exchange says otherwise. Round when you invoice.
- Handlers are synchronous with the call and async handlers aren't awaited. Write the line somewhere durable (a queue, a table) and do slow work, like calling a payment API, after. A handler that throws is logged and skipped, and the call still succeeds, so a failed write never looks like a failed call.
- Report usage, not invoices. Most payment providers take metered usage and build the invoice: send each line as a usage record with the customer, the item, and the quantity or amount.
- Reconcile at the end of the period.
value,limit, andcustomerMarginSnapshotread the period's usage, revenue, and cost, so you can check the ledger against the meters.
Who pays
Bill the customer the event names. For an entitlement with scope: 'org', that's the org, whichever user made the
call. For entitlements metered per user, roll users up to their org through their refs in your billing code.