Skip to content
ida314Public

About

Small quota + credit enforcement for AI/API products without a billing platform.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

uselimit

Tiny usage credits and quota enforcement for AI/API products.

CI License: MIT Types: TypeScript npm version

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 + deduct

Why this exists

You'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/month on 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.


Features

  • 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, or never reset windows.
  • Unlimited tiers — null credits / null limits 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).

Quick start

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) // 97

Persist to disk

InMemoryAdapter 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 default

Nothing else changes. See @uselimit/storage-sqlite for options and its (single-host) limits.

In a request handler

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/:


Plan configuration

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.


Current status

Early, but functional — not a stub. Both packages are implemented and tested:

  • ✅ check(), consume(), and exportUsage() (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 StorageAdapter interface 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 periodStart or refills a balance at resetAt, so a monthly quota accumulates forever and nextReset() 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.


When to use this

  • 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.

When not to use this

  • 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.

Roadmap

Phase 1 — Core (@uselimit/core)

  • Domain type definitions
  • StorageAdapter interface
  • InMemoryAdapter implementation
  • 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)

Phase 2 — Storage adapters

  • @uselimit/storage-sqlite
  • @uselimit/storage-postgres
  • @uselimit/storage-redis

Phase 3 — Framework adapters

  • @uselimit/next — Next.js middleware + route helpers
  • @uselimit/express — Express middleware
  • @uselimit/hono — Hono middleware

Phase 4 — Export integrations

  • Stripe metered-billing push
  • Webhook on threshold exceeded

Phase 5 — Dashboard

  • Minimal read-only usage dashboard

Packages

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

Contributing

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.

License

MIT

About

Small quota + credit enforcement for AI/API products without a billing platform.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages