Bismite docs
Gate, meter & monetize any feature in 3 lines — and it never takes your app down.
The billing runtime that lives in your code: on every request it answers is this user allowed, and how much have they used? — with the upgrade loop wired through Stripe.
Install
npm install bismite
Zero dependencies, ESM, ships types. Node 18+ / any serverless runtime.
Quickstart
Get one API key, define plans as code, then gate before and record after. The usage counter is hosted for you — no Redis to provision, no second vendor.
1. Get an API key
Sign up at app.bismite.dev, create a project, and copy its live key. Drop it in your env:
# .env.local
BISMITE_API_KEY=bsk_live_...
bsk_test_… is an isolated namespace that doesn't count toward billing — use it for local dev and CI.
2. Plans & client — bismite.config.ts
import { Billing } from "bismite";
import { bismiteCounter } from "bismite/hosted";
// Plans as code — per-feature limits, defined here in your repo.
export const plans = {
free: { features: { "chat-message": { limit: 20, period: "day" } } },
pro: { features: { "chat-message": "unlimited" } },
};
export const bismite = new Billing({
plans,
resolvePlan: (userId) => "free", // real plan in step 4 (Stripe)
counter: bismiteCounter(process.env.BISMITE_API_KEY!), // hosted, managed
upgradeUrl: (userId) => `/api/checkout?userId=${encodeURIComponent(userId)}`,
});
3. Gate before, meter after
const access = await bismite.check(userId, "chat-message"); // gate
if (!access.allowed) {
return Response.json({ upgradeUrl: access.upgradeUrl }, { status: 402 });
}
const reply = await callYourLLM(message); // the thing that costs money
await bismite.record(userId, "chat-message", { count: 1 }); // meter
Free users get 20/day, then a 402 with an upgrade URL. Watch usage climb and upgrade in the dashboard. That's a working gate + meter.
Units: count vs tokens
A limit counts one of two things. The default counts calls — N requests per period. For AI features the limit is usually tokens: add unit: "tokens" and record the real usage after the call returns.
// plan: each user gets 50k tokens/day on free
free: { features: { "chat-message": { limit: 50_000, period: "day", unit: "tokens" } } }
const access = await bismite.check(userId, "chat-message"); // gate on tokens-so-far
if (!access.allowed) return Response.json({ upgradeUrl: access.upgradeUrl }, { status: 402 });
const completion = await openai.chat.completions.create({ /* ... */ });
await bismite.record(userId, "chat-message", { tokens: completion.usage.total_tokens });
check() runs before the call, when the token cost is still unknown — so you gate on usage-so-far and meter the actual after. remaining is expressed in the rule's unit (tokens here, calls otherwise). A token feature recorded without { tokens } meters nothing.
API reference
check(userId, feature) → Promise<CheckResult>
Gate a feature before the expensive work. Never throws.
| Field | Type | Meaning |
|---|---|---|
allowed | boolean | Whether the user may proceed. |
remaining | number | Units left this period (Infinity if unlimited; -1 when the meter is down and the feature fails open). |
upgradeUrl | string | null | Where to send a blocked user. null when allowed. |
overLimit | boolean | Advisory, from the hosted counter: your project is over its Bismite tier (MTU) allowance — orthogonal to allowed (the user's own plan rule). Never blocks; surface an "upgrade your Bismite plan" nudge. Always false on a self-hosted/BYO counter. |
record(userId, feature, usage?) → Promise<void>
Meter usage after the work (token counts are only known once the call returns). Never throws — a failed record is swallowed so it can't break the caller.
| Param | Type | Meaning |
|---|---|---|
usage.count | number | Increment for count features. Defaults to 1. |
usage.tokens | number | Increment for tokens features. Defaults to 0 (no-op). |
Billing config
Passed to new Billing({ ... }).
| Field | Type | Meaning |
|---|---|---|
plans | Record<string, Plan> | Plan → feature → rule. A rule is { limit, period, unit?, failClosed? } or "unlimited". |
resolvePlan | (userId) => string | The user's current plan name. Sync or async. In production this reads the plan your Stripe webhook wrote. |
counter | CounterClient | The usage meter backend (see below). |
upgradeUrl | (userId, feature) => string | Optional. URL returned on a blocked check(). |
FeatureRule
| Field | Type | Meaning |
|---|---|---|
limit | number | Allowance per period, in the rule's unit. |
period | "day" | "month" | When the counter resets (UTC boundary, no cron). |
unit | "count" | "tokens" | Optional. What the limit counts. Defaults to "count". |
failClosed | boolean | Optional. Block when the meter is unreachable instead of failing open. See Failure modes. |
Counters
The counter is the managed runtime — bismiteCounter(apiKey) and you're done. It's also a two-method seam, so you're never locked in: self-host it anytime without touching the rest of your config.
import { bismiteCounter } from "bismite/hosted"; // hosted, managed (default)
import { upstashCounter } from "bismite/redis-counter"; // self-host: bring your own Upstash
import { httpCounter } from "bismite/http-counter"; // self-host: point at your own service
// or any object shaped like this:
interface CounterClient {
read(key: string): Promise<number | { used: number; overLimit?: boolean }>;
increment(key: string, amount: number): Promise<void>;
}
bismiteCounter(apiKey, baseUrl?) defaults its baseUrl to https://api.bismite.dev; pass your own to self-host the counter service. It throws on any failure, so check()/record() fail open — a down counter never takes your app down.
Self-host — you're never locked in
Bring your own Upstash Redis (free tier, REST API): create a database and swap one line —
// counter: bismiteCounter(process.env.BISMITE_API_KEY!), ← swap this line
counter: upstashCounter(process.env.UPSTASH_REDIS_REST_URL!, process.env.UPSTASH_REDIS_REST_TOKEN!),
upstashCounter uses atomic INCRBY so concurrent records never lose or double-count; keys are period-scoped and expire automatically. Or implement CounterClient against any backend you like — same gate, same meter, your infra.
Stripe upgrade loop
To make the paywall take money, wire three things so an upgrade flips the plan with no deploy:
/api/checkout— create a Stripe Checkout Session withclient_reference_id = userId, redirect to it./api/stripe/webhook— oncheckout.session.completedandcustomer.subscription.*, write the user's new plan to a store thatresolvePlanreads.- Reconciliation — re-pull the plan from Stripe on demand, so a missed webhook never locks out a paying customer.
All three are implemented and live-verified in the Next.js example.
Failure modes, on purpose
| Situation | What happens |
|---|---|
| Counter unreachable | Fail open — users pass, you eat a small usage leak instead of an outage. This is the product promise. |
| Expensive feature, strict enforcement needed | Set failClosed: true on that one rule — it blocks when the meter is down. |
| Missed Stripe webhook | Run reconciliation; the plan re-syncs from Stripe. |
| New billing period | Counters are period-scoped, so the limit resets cleanly on the boundary — no cron. |