Prepaid credits
Customers buy credits, and each feature spends them at its own rate: an image is 4 credits, a second of video is 10, 1,000 AI tokens are 2. One balance pays for everything.
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
policy: {
exchange: {
studio_credit: { value: 0.01, currency: 'usd' } // 1 credit is 1 cent
}
credits: {
studio_credit: { label: 'Studio credit' }
// What each feature costs: 4 credits an image, 2 credits
// per 1,000 tokens, and 10 credits a second of video.
image: { label: 'Image', price: { amount: 0.04 } }
ai_token: { label: 'AI token', price: { amount: 0.00002 } }
video_second: { label: 'Video second', price: { amount: 0.10 } }
}
plans: {
creator: {
default: true
entitlements: {
// Nothing included: every feature runs on credits.
images: { limit: { credit: 'image', value: 0 } }
chat: { limit: { credit: 'ai_token', value: 0 } }
video: { limit: { credit: 'video_second', value: 0 } }
}
topups: {
monthly_credits: {
credit: 'studio_credit', value: 1000, included: true
resets: true, reset_sch: 'monthly:1'
}
credits_5000: {
credit: 'studio_credit', value: 5000, price: { amount: 45 }
}
}
}
}
}`);
await limitr.ensureCustomer('ada');
// Ada's balance, in credits.
async function balance() {
const credits = await limitr.remainingCredit('ada', 'studio_credit');
return Math.round(credits!);
}
// Use a feature, and show what's left.
async function use(label: string, entitlement: string, amount: number) {
const allowed = await limitr.allow('ada', entitlement, amount);
console.log(`${label}:`, allowed, '| balance', await balance());
}
console.log('monthly credits:', await balance());
await use('50 images', 'images', 50);
await use('100k tokens', 'chat', 100_000);
await use('60s of video', 'video', 60);
await use('20s more', 'video', 20);
// After the payment goes through
await limitr.applyCustomerTopup('ada', 'credits_5000');
console.log('after buying 5,000:', await balance());
await use('20s more', 'video', 20);
It prints:
monthly credits: 1000
50 images: true | balance 800
100k tokens: true | balance 600
60s of video: true | balance 0
20s more: false | balance 0
after buying 5,000: 5000
20s more: true | balance 4800
How it works
- The credit is abstract.
studio_credithas no price of its own:{ value: 0.01, currency: 'usd' }in the exchange says one is worth a cent. The features stay discrete credits, each metered and priced on its own. - Each feature has a price, on its own credit: an image is $0.04, so it spends 4 studio credits.
- Nothing is included. Hard limits at 0 mean every call runs on grants, and stops when they run out. The fourth call was denied: 20 seconds of video needed 200 credits, and the balance was 0.
- Credits arrive as grants: an included topup refilled every month, and packs applied after a purchase.
remainingCredit(id, 'studio_credit')is the balance, for your UI.
For AI models, the same setup works with a credit per model and direction: Sell one AI credit.
Variations
| Need | How |
|---|---|
| Unused monthly credits carry over | reset_mode: 'rollover' on the included topup, with rollover_pct or rollover_max. |
| Packs expire | expires_after: 365days on the pack. |
| Spend expiring credits first | exchange: { grant_strategy: 'expires_first' } (the default). |
| Keep going past zero, and bill it | Soft limits instead of hard: past the grants, usage is overage. |
| A free allowance before credits | A limit above 0: usage inside it doesn't spend credits. |
| Warn when the balance is low | Check remainingCredit after calls, or warn on spend with an observe_only cap. |
| Stop a safety limit being bought past | grants_apply: false on that limit. |