Usage Limits and Pricing Plans
Pricing logic ends up everywhere. The API checks limits before each request, the billing job computes overages, the app shows "you've used 80%", and the pricing page lists what each plan includes. Each one has its own copy of the rules, and every pricing change is a coordinated deploy that someone gets slightly wrong.
Put the plans in one Stof document: the numbers and the logic that uses them. Every service loads the same document and asks it questions, in-process, in a fraction of a millisecond. To change pricing, publish a new document.
The Plans
Each plan is an object of type Plan. The type holds the logic, and each plan holds its numbers. Press Run to check some usage against two plans:
#[type]
Plan: {
str name: ''
float price: 0 // per month
int requests: 0 // included each month
float overage: 0 // per request over the limit; 0 means a hard limit
GB storage: 1GB
/// Can this request go ahead, given what's been used this month?
fn check(used: map) -> map {
const requests = used.get('requests') + 1;
const storage = used.get('storage_mb') as MB;
if (storage > self.storage) {
return {'allowed': false, 'reason': `storage is full (${self.storage} on ${self.name})`};
}
if (requests > self.requests && self.overage == 0) {
return {'allowed': false, 'reason': `${self.name} includes ${self.requests} requests a month`};
}
{'allowed': true, 'percent_used': ((requests as float) / self.requests * 100).round(1)}
}
/// This month's bill.
fn bill(used: map) -> float {
const extra = Num::max(used.get('requests') - self.requests, 0);
(self.price + extra * self.overage).round(2)
}
}
Plan free: {
name: 'Free'
requests: 100
}
Plan pro: {
name: 'Pro'
price: 20
requests: 5_000
overage: 0.002
storage: 10GB
}
#[main]
fn main() {
const used = {'requests': 6_200, 'storage_mb': 2_400};
pln(self.pro.check(used));
pln(self.free.check(used));
pln(self.pro.bill(used));
}Units do the conversions: usage arrives in megabytes, the plan says 10GB, and the comparison is exact. A new plan is a few lines of numbers, and a new kind of limit is a few lines in check.
Checking Every Request
The API server loads the plans once and asks them about each request. It's a function call inside your process, so there's no pricing service to call and nothing to cache:
import { StofDoc } from '@formata/stof';
let pricing = await StofDoc.new(await fetchPublishedPlans(), 'bstf');
pricing.setMaxExecutionTime(20);
export async function allow(account: Account, used: Usage) {
const decision = await pricing.call(`${account.plan}.check`, used) as Map<string, unknown>;
return Object.fromEntries(decision); // { allowed: true, percent_used: 41.2 }
}
app.use(async (req, res, next) => {
const decision = await allow(req.account, await usage.thisMonth(req.account));
if (!decision.allowed) return res.status(429).json({ error: decision.reason });
next();
});
The billing job loads the same document and calls bill. The front end can load it too, through the npm package in the browser, to show usage bars and to power the pricing page, and every one of them gives the same answer.
Changing Plans Without a Deploy
The pricing service publishes the plans as binary Stof, which carries the logic along with the numbers. Every service swaps in the new document when it arrives:
// pricing service: publish the current plans
const plans = await StofDoc.new(plansSource);
await publish('pricing', plans.blobify('bstf'));
// every service: swap in new plans as they arrive
subscribe('pricing', async (bytes: Uint8Array) => {
const next = await StofDoc.new(bytes, 'bstf');
next.setMaxExecutionTime(20);
const old = pricing;
pricing = next;
old.dispose();
});
Raising the Free plan to 500 requests, adding a plan for a single customer, or trying a new overage price is a new document, not a release. Keep the plans in version control with tests (#[test] functions that check a few bills), and publish from CI.
Going Further: Limitr
This is the core of Limitr, a pricing runtime for usage-based and AI software built on Stof. Limitr adds what a real pricing system needs around it: metering, credits, costs and margins for AI usage, an event ledger, invoicing, and a cloud service that streams usage from every running app and pushes policy changes back to it. Each plan, limit, and credit rule in Limitr is a Stof document evaluated in-process, exactly like this one.
Why This Works
- One source of truth. Every service asks the same document, so the API, billing, and the pricing page can't disagree.
- Fast. A check runs in your process in a fraction of a millisecond (about 0.2ms in Node.js), with no network call.
- Changes are data. New plans and prices ship as a document, in seconds, without a deploy.
- Readable. The rules are short enough for the people who set pricing to review.
The same approach works for feature flags and entitlements, rate limits, quotas, and any other rule that many services need to agree on.