Skip to main content

Quickstart

Install​

npm i @formata/limitr

Limitr runs in Node, Bun, Deno, and the browser. The package includes the engine, so there's nothing else to run.

Your first policy and calls​

Save this as quickstart.mts and run it with npx tsx quickstart.mts (or Bun or Deno):

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

// 1. The policy: what each plan includes, what it costs you, and what you charge.
const limitr = await Limitr.new(`
policy: {
credits: {
request: { label: 'API request' }
ai_token: { label: 'AI token', price: { amount: 0.00002 }, overhead_cost: 0.000008 }
}
plans: {
free: {
default: true
entitlements: {
api: {
limit: {
credit: 'request', value: 1000
resets: true, reset_inc: 1day
}
}
chat: {
limit: {
credit: 'ai_token', value: 50_000
resets: true, reset_sch: 'monthly:1'
}
}
}
}
pro: {
entitlements: {
api: {
limit: {
credit: 'request', value: 100_000
resets: true, reset_sch: 'monthly:1'
}
}
chat: {
limit: {
credit: 'ai_token', mode: 'soft', value: 2_000_000
resets: true, reset_sch: 'monthly:1'
}
}
export_pdf: {}
}
}
}
}`);

// 2. Events: bill overage as it happens.
limitr.addHandler('billing', (name, value) => {
if (name === 'meter-overage') {
const event = JSON.parse(value as string);
console.log(`bill ${event.customer.id}: $${event.overage_price}`);
}
});

// 3. Customers: created on first sight, on the default plan unless you say otherwise.
await limitr.ensureCustomer('ada');
await limitr.ensureCustomer('acme', 'pro');

// 4. Ask before doing anything that costs money.
console.log('ada, 1 API request:', await limitr.allow('ada', 'api', 1));
console.log('ada, PDF export:', await limitr.allow('ada', 'export_pdf'));
console.log('ada, 60k tokens:', await limitr.allow('ada', 'chat', 60_000));
console.log('acme, PDF export:', await limitr.allow('acme', 'export_pdf'));
console.log('acme, 2.1M tokens:', await limitr.allow('acme', 'chat', 2_100_000));
console.log('acme tokens left:', await limitr.remaining('acme', 'chat'));

It prints:

ada, 1 API request: true
ada, PDF export: false
ada, 60k tokens: false
acme, PDF export: true
bill acme: $2
acme, 2.1M tokens: true
acme tokens left: -100000

What happened​

  • The policy has two credits (the things you count) and two plans. Each plan lists entitlements: what its customers can do. export_pdf has no limit, so it's a feature flag: Pro has it, Free doesn't. api and chat have limits that reset daily or monthly.
  • Ada was created without a plan, so she's on the default plan (Free). Her PDF export was denied (not on her plan), and so was 60k tokens: Free's chat limit is hard, at 50,000 a month.
  • Acme is on Pro, where chat is soft: the call went 100,000 tokens past the 2 million included, was allowed, and the overage was billed through the meter-overage event, at the credit's price ($20 per million).
  • allow decides and, when the answer is yes, counts the usage. A denied call changes nothing. Unknown customers and entitlements are denied; nothing throws.

In your app​

Create the engine once when your server starts, and ask in one place, like a middleware:

limitr.ts
import { Limitr } from '@formata/limitr';
import { readFileSync } from 'node:fs';

export const limitr = await Limitr.new(readFileSync('./policy.stof', 'utf8'));

export async function gate(userId: string, entitlement: string, amount = 1): Promise<boolean> {
await limitr.ensureCustomer(userId);
return limitr.allow(userId, entitlement, amount);
}
  • Keep one engine per process. It holds customer state in memory, which is why calls are fast.
  • Save customer state if you self-host: see Save customer state. Or use Limitr Cloud, where Limitr.cloud({ token }) replaces Limitr.new and state is managed for you.
  • Await every call. if (limitr.allow(...)) without await is always true.

Next​