Grants and topups
A topup is a credit pack defined on a plan: 100,000 AI tokens for $1.50, 100 images for $3, 5,000 bonus tokens every month. When a customer gets one, the engine creates a grant: a balance of that credit on the customer.
Grants cover usage past a limit:
- On a soft limit, overage is drawn from grants first, and only the rest is billed.
- On a hard limit, calls past the limit run on grants until the balance runs out.
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
policy: {
credits: {
ai_token: { label: 'AI token', price: { amount: 0.00002 } }
}
plans: {
pro: {
default: true
entitlements: {
chat: {
limit: {
credit: 'ai_token', mode: 'soft', value: 10_000
resets: true
}
}
}
topups: {
// 5,000 bonus tokens every month, included with the plan
monthly_bonus: {
credit: 'ai_token', value: 5_000, included: true
resets: true, reset_sch: 'monthly:1'
}
// a pack customers can buy, good for 90 days
tokens_100k: {
credit: 'ai_token', value: 100_000, price: { amount: 1.5 }
expires_after: 90days
}
}
}
}
}`);
limitr.addHandler('billing', (name, value) => {
const event = JSON.parse(value as string);
const covered = event.grant_value_applied ?? 0;
if (name === 'meter-changed' && covered) {
console.log(` grants covered ${covered}`);
}
if (name === 'meter-overage') {
const bill = `bill ${event.overage} tokens for $${event.overage_price}`;
console.log(` grants covered ${covered}, ${bill}`);
}
});
await limitr.ensureCustomer('ada');
console.log('balance:', await limitr.remainingCredit('ada', 'ai_token'));
console.log('call 1: 12,000 tokens');
await limitr.allow('ada', 'chat', 12_000);
console.log('call 2: 6,000 tokens');
await limitr.allow('ada', 'chat', 6_000);
await limitr.applyCustomerTopup('ada', 'tokens_100k');
console.log('after buying a pack:', await limitr.remainingCredit('ada', 'ai_token'));
console.log('call 3: 20,000 tokens');
await limitr.allow('ada', 'chat', 20_000);
console.log('balance:', await limitr.remainingCredit('ada', 'ai_token'));
It prints:
balance: 5000
call 1: 12,000 tokens
grants covered 2000
call 2: 6,000 tokens
grants covered 3000, bill 3000 tokens for $0.06
after buying a pack: 100000
call 3: 20,000 tokens
grants covered 20000
balance: 80000
- The included bonus was granted when Ada was created.
- Call 1 went 2,000 past the 10,000 limit, and the bonus covered all of it, so nothing was billed (
meter-changed). - Call 2 used the last 3,000 of the bonus, and the other 3,000 were billed (
meter-overage). - Call 3 ran entirely on the purchased pack.
Topup fields
| Field | Type | Default | Notes |
|---|---|---|---|
credit | str | (required) | The credit granted. |
value | number or units | 0 | How much. |
price | Price | null | What the pack costs. For your checkout; the engine doesn't charge it. |
description | str | '' | |
included | bool | false | Granted with the plan, for free. |
included_scopes | list | null | Customer types an included topup is granted to (Ex. ['org']). Null: every type. |
plan_change | str | 'top_up' | What an included balance does when a customer moves onto this plan: top_up, replace, or keep (Plan changes). |
expires_after | duration | null | The grant expires this long after it's granted (Ex. 90days). |
resets | bool | false | Refill the grant each period. |
reset_inc / reset_sch | The period, as on a limit. | ||
reset_mode | str | 'hard' | hard: back to value. add: add value. rollover: carry some over, then add. |
rollover_pct | float | null | With rollover: the share of the balance carried (Ex. 0.5). |
rollover_min / rollover_max | number | null | With rollover: clamp the carried amount. |
max_balance | number | null | The most a grant can hold after a reset. |
reset_catchup_cap | int | null | The most missed periods caught up at once. |
Getting grants
| Call | Does |
|---|---|
applyCustomerTopup(id, topup) | Grants a topup from the customer's plan. Call it after your payment provider confirms the purchase. Sends customer-topup-granted. |
ensureCustomerIncludedTopups(id) | Grants included topups the customer is missing. Creating a customer and changing its plan already do this. |
remainingCredit(id, credit) | The balance in a credit, with grants in other credits converted through the exchange. |
How grants are spent
- Which credit. A grant covers usage in its own credit, and in other credits it converts to through the exchange. That's how one prepaid "AI credit" balance pays for images and tokens at their own prices (Prepaid credits).
- Which grant first.
exchange.grant_strategy:expires_first(default),cheapest_first, orvaluable_first. - Where they apply. A limit with
grants_apply: falseignores grants. Use it on safety limits (rate limits, abuse ceilings) so a purchase never lifts them. - What gets billed.
meter-overagereportsgrant_value_applied(what grants covered) andoverage_price(what they didn't, priced). When grants cover everything, the call sendsmeter-changedinstead. - Reads.
remainingandlimitinclude grants by default. Passgrants: falsefor the plan alone.
Grants follow their topup
A grant from a topup keeps following its definition: change the topup on the plan and its grants change, remove it and its grants are removed. Expired grants are cleaned up on the next read or call.
The grant record (customer.grants) has the balance (value), starting_value, granted_on, expires_on, the
topup and plan it came from (topup, plan), and the reset settings copied from the topup.
Plan changes
When a customer changes plans (setCustomerPlan), its grants move with it:
-
Included balances carry over. A grant from the old plan's included topup moves into the new plan's included topup in the same credit, by that topup's
plan_change:plan_changeThe balance top_up(default)Rises to the new topup's value, and is never lowered. An upgrade tops up; a downgrade keeps the larger balance.replaceStarts over at the new topup's value.keepStays as it is. From then on, the grant follows the new topup: its resets, expiry, and rollover.
-
No matching topup, no balance. An included balance is removed when the new plan has no included topup in that credit for the customer's type (
included_scopes). To carry it anyway, add one to the new plan, Ex.{ credit: 'ai_token', value: 0, included: true, plan_change: 'keep' }keeps the balance until its next reset. -
New included topups the customer doesn't have yet are granted.
-
Purchased grants stay. Grants from
applyCustomerTopupbelong to the customer: they stay through every plan change and keep following the topup they were bought from.
pro: {
topups: {
// moving onto Pro: top up to 250,000 (the default)
monthly_tokens: {
credit: 'ai_token', value: 250_000, included: true
resets: true, reset_sch: 'monthly:1'
}
}
}
annual: {
topups: {
// moving onto Annual: start over at 3,000,000, whatever was left
yearly_tokens: {
credit: 'ai_token', value: 3_000_000, included: true, plan_change: 'replace'
}
}
}
See Change plans for a walkthrough.