SDK-first billing for AI apps

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.

The hosted counter is atomic, shared across instances, and the key resolves to your project server-side — so one key only ever touches your counts. Prefer to run it yourself? You're never locked in — see Counters.

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.

FieldTypeMeaning
allowedbooleanWhether the user may proceed.
remainingnumberUnits left this period (Infinity if unlimited; -1 when the meter is down and the feature fails open).
upgradeUrlstring | nullWhere to send a blocked user. null when allowed.
overLimitbooleanAdvisory, 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.

ParamTypeMeaning
usage.countnumberIncrement for count features. Defaults to 1.
usage.tokensnumberIncrement for tokens features. Defaults to 0 (no-op).

Billing config

Passed to new Billing({ ... }).

FieldTypeMeaning
plansRecord<string, Plan>Plan → feature → rule. A rule is { limit, period, unit?, failClosed? } or "unlimited".
resolvePlan(userId) => stringThe user's current plan name. Sync or async. In production this reads the plan your Stripe webhook wrote.
counterCounterClientThe usage meter backend (see below).
upgradeUrl(userId, feature) => stringOptional. URL returned on a blocked check().

FeatureRule

FieldTypeMeaning
limitnumberAllowance 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".
failClosedbooleanOptional. 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:

  1. /api/checkout — create a Stripe Checkout Session with client_reference_id = userId, redirect to it.
  2. /api/stripe/webhook — on checkout.session.completed and customer.subscription.*, write the user's new plan to a store that resolvePlan reads.
  3. 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

SituationWhat happens
Counter unreachableFail open — users pass, you eat a small usage leak instead of an outage. This is the product promise.
Expensive feature, strict enforcement neededSet failClosed: true on that one rule — it blocks when the meter is down.
Missed Stripe webhookRun reconciliation; the plan re-syncs from Stripe.
New billing periodCounters are period-scoped, so the limit resets cleanly on the boundary — no cron.

npm install bismite · GitHub · Home