Tiny usage credits and quota enforcement for AI/API products.
Add per-tenant credits, per-feature quotas, and usage export to your app in an afternoon — before you reach for a full billing platform or an enterprise API gateway.
// Will this request blow the user's budget? Block it before you pay for the work.
const pre = await usage.check({ tenantId, feature: 'generate-image', cost: 12 })
if (!pre.allowed) return res.status(402).json({ error: pre.reason })
const image = await generateImage(prompt) // your expensive AI/API call
await usage.consume({ tenantId, feature: 'generate-image', amount: 12 }) // record + deductYou're building an AI SaaS, a devtool with free/pro tiers, or an internal LLM tool. Every request can cost you real money, so you need to:
- Cap spend — block a request before it exceeds a tenant's monthly credit budget.
- Gate features —
summaries: 20/monthon free, unlimited on pro. - Meter per tenant and per user — know who used what, and how much.
- Export usage — hand events to Stripe metered billing, a warehouse, or a CSV later.
You don't need invoices, tax handling, or a traffic-routing gateway yet. You need a small, typed layer that answers one question:
Can this tenant do this, right now?
That's uselimit.
check()/consume()— read-only pre-flight and a write path that deducts credits and records an event.- Tenant & user balances — credits at the tenant level, with optional per-user plan overrides.
- Per-feature quotas —
daily,monthly,rolling_24h,rolling_30d, orneverreset windows. - Unlimited tiers —
nullcredits /nulllimits for pro/enterprise plans. - Immutable usage events — the single source of truth for both quotas and exports.
- Usage export — JSON or RFC 4180 CSV, filterable by tenant/user/feature/date.
- Atomic consumption — no double-spend under concurrency, on any adapter that supports it.
- Pluggable storage — implement one small
StorageAdapter; in-memory and SQLite ship in the box. - TypeScript-first — fully typed, with zero runtime dependencies (SQLite included — it uses Node's builtin
node:sqlite).
Heads up: these packages aren't on npm yet. For now, clone this repo and build them, or link them into your project. Once published, install will be
pnpm add @uselimit/core @uselimit/storage-sqlite.
import { UseLimitClient, InMemoryAdapter } from '@uselimit/core'
// 1. Pick a storage adapter. InMemoryAdapter is perfect for local dev and tests.
const storage = new InMemoryAdapter()
// 2. Seed a tenant and its credit balance (your app does this on signup).
await storage.upsertTenant({
id: 'team_abc',
planId: 'free',
periodStart: '2026-06-01T00:00:00.000Z',
createdAt: '2026-06-01T00:00:00.000Z',
})
await storage.setBalance({
tenantId: 'team_abc',
remaining: 100,
allocated: 100,
resetAt: '2026-07-01T00:00:00.000Z',
})
// 3. Create the client with your plan definitions.
const usage = new UseLimitClient({
storage,
plans: [
{
id: 'free',
monthlyCredits: 100,
limits: {
summaries: { max: 20, resetPeriod: 'monthly' },
},
},
],
})
// 4. Pre-flight check — read-only, deducts nothing.
const pre = await usage.check({ tenantId: 'team_abc', feature: 'summaries', cost: 3 })
if (!pre.allowed) throw new Error(pre.reason) // e.g. "Insufficient credits"
// 5. ...run the expensive operation...
// 6. Record usage — deducts credits and writes an immutable event.
const result = await usage.consume({ tenantId: 'team_abc', feature: 'summaries', amount: 3 })
console.log(result.remaining) // 97InMemoryAdapter loses everything on restart. Swap in the SQLite adapter and the same code
keeps its balances and usage events — and gains atomic consumption:
import { SqliteAdapter } from '@uselimit/storage-sqlite'
const storage = new SqliteAdapter('./usage.db') // ':memory:' by defaultNothing else changes. See @uselimit/storage-sqlite for options
and its (single-host) limits.
The realistic shape is check → operate → consume: refuse early when the budget is gone, and only charge for work you actually did.
async function handler(req, res) {
const tenantId = req.headers['x-tenant-id']
// Block before spending money.
const pre = await usage.check({ tenantId, feature: 'generate-image', cost: 12 })
if (!pre.allowed) {
return res.status(402).json({ error: pre.reason, resetAt: pre.resetAt })
}
const image = await generateImage(req.body.prompt) // your expensive call
// Charge only after success.
await usage.consume({ tenantId, feature: 'generate-image', amount: 12 })
return res.json({ image })
}Runnable versions of this live in examples/:
examples/express-api— a protected Express route using anx-tenant-idheader.examples/nextjs-ai-route— a Next.js App Router route handler.examples/sqlite-persistence— run it twice; the balance picks up where it left off.
Today, plans are plain typed objects:
import type { Plan } from '@uselimit/core'
const plans: Plan[] = [
{
id: 'free',
monthlyCredits: 100,
limits: {
summaries: { max: 20, resetPeriod: 'monthly' },
exports: { max: 5, resetPeriod: 'monthly' },
},
},
{
id: 'pro',
monthlyCredits: 5000,
limits: {
summaries: { max: null, resetPeriod: 'monthly' }, // null = unlimited
exports: { max: 500, resetPeriod: 'monthly' },
},
},
]A YAML/JSON plan-config loader (so you can write summaries: 20/month) is on the roadmap.
Early, but functional — not a stub. Both packages are implemented and tested:
- ✅
check(),consume(), andexportUsage()(JSON + CSV). - ✅ Two storage adapters: in-memory, and persistent SQLite.
- ✅ Atomic consumption — concurrent
consume()calls can't double-spend, verified in-process and across OS processes. - ✅ Domain types and the
StorageAdapterinterface are stable, with a shared conformance suite (@uselimit/core/testing) that both adapters pass.
Honest limitations:
- 🟡 Quotas and credits never reset. Nothing advances a tenant's
periodStartor refills a balance atresetAt, so a monthly quota accumulates forever andnextReset()reports a past date once a tenant is more than a month old. Refill from your own scheduler for now. This matters more with SQLite than it did in memory, where a restart re-seeded everything — it's the next thing on the roadmap. - 🟡 SQLite is single-host. Multiple processes on one machine are fine; a network filesystem is not a supported deployment, and writes are serialized (see the adapter README). Postgres/Redis are planned.
- 🟡
check()is advisory, not enforcement. It's inherently racy against concurrent writers — it exists so you can return a 402 before doing expensive work.consume()is the enforcement point. - 🟡 A third-party adapter without
tryConsume()falls back to a read-modify-write path that is subject to double-spend. Both bundled adapters implement it. - 🟡 No framework adapters or YAML plan loader yet.
- 🟡 Not yet published to npm.
If those gaps matter for your use case, see the roadmap below or open an issue — contributions welcome.
- You're adding credits/quotas to an AI or API product and want a small, typed library, not a billing suite.
- You already have a database and just need the metering logic plus a storage interface to plug into.
- You want immutable usage events you can export to Stripe / a warehouse later.
- You're on a single host — one process or several — and want credits that survive a restart.
- You need payment processing, invoices, taxes, or subscription lifecycle → use Stripe Billing, Orb, Lago, or OpenMeter.
- You need a distributed rate limiter (multi-region sliding windows).
- You need an API gateway / proxy that routes traffic.
- You need multi-host storage today — SQLite is single-host, and the Postgres and Redis adapters aren't written yet.
- You need automatic quota/credit resets today — see the limitations above.
- Domain type definitions
-
StorageAdapterinterface -
InMemoryAdapterimplementation -
check()— read-only pre-flight -
consume()— credit deduction + event recording -
exportUsage()— JSON/CSV export - Concurrency-safe consume contract (
StorageAdapter.tryConsume()) - Shared adapter conformance suite (
@uselimit/core/testing) - Lazy quota-reset helpers ← next
- Plan config loader (YAML/JSON)
-
@uselimit/storage-sqlite -
@uselimit/storage-postgres -
@uselimit/storage-redis
-
@uselimit/next— Next.js middleware + route helpers -
@uselimit/express— Express middleware -
@uselimit/hono— Hono middleware
- Stripe metered-billing push
- Webhook on threshold exceeded
- Minimal read-only usage dashboard
| Package | Description | Status |
|---|---|---|
@uselimit/core |
Core logic, types, StorageAdapter, InMemoryAdapter |
✅ Implemented |
@uselimit/storage-sqlite |
Persistent SQLite adapter on builtin node:sqlite |
✅ Implemented (Node ≥ 22.13) |
@uselimit/storage-postgres |
Postgres storage adapter | Planned |
@uselimit/storage-redis |
Redis storage adapter | Planned |
@uselimit/next |
Next.js adapter | Planned |
Contributions are welcome — see CONTRIBUTING.md for setup, how to run the
checks, and how to add examples. Good first issues: the lazy quota-reset helpers, a Postgres or
Redis StorageAdapter (run @uselimit/core/testing's conformance suite against it), or the
YAML plan loader.