Usage limits, feature gates, and metering for AI products. Drop-in for FastAPI and Next.js.
Project description
Cupo
Usage limits, feature gates, and metering for AI products. Drop-in for FastAPI and Next.js.
⚠️ Status: design phase / request for comments. This README describes the API we intend to build. We're validating the design before writing the code — if this would (or wouldn't) solve a problem for you, please open an issue or comment. Brutal honesty welcome.
The problem
Billing platforms (Stripe, Lago, Metronome, Orb) answer "how much do I charge?" — they count usage and generate invoices after the fact.
None of them answer the question your code asks a hundred times per second: "is this customer allowed to do this, right now?"
So every AI SaaS ends up hand-rolling the same thing:
- A
usagetable with counters that break under concurrent requests - Plan limits scattered across
if customer.plan == "pro"checks - Token counting glued onto LLM calls (and silently wrong for streaming)
- A cron job that resets counters monthly (usually in the wrong timezone)
- No warning to the customer before they hit the wall
Cupo is that layer, done once, done right, and open source.
What it looks like
from cupo import Cupo
cupo = Cupo() # embedded mode: uses your existing Postgres, no server needed
@app.post("/chat")
@cupo.protect(feature="ai_chat") # blocks with 429 + upgrade info if over limit
async def chat(req: ChatRequest, customer: Customer):
...
Or with explicit control:
res = cupo.check(customer.id, feature="ai_chat", units=1) # atomic check-and-consume
if not res.allowed:
return JSONResponse(429, {"error": "plan_limit", "resets_at": res.resets_at,
"upgrade_url": res.upgrade_url})
reply = anthropic.messages.create(...)
cupo.track(customer.id, feature="ai_chat",
tokens=reply.usage.output_tokens,
idempotency_key=req.id) # safe to retry, never double-counts
Plans as code
Plans live in a versioned YAML file in your repo — reviewable in a PR, not hidden in a dashboard:
# cupo.yaml
features:
ai_chat: { unit: message }
ai_tokens: { unit: token }
pdf_export: { unit: export }
plans:
free:
ai_chat: { limit: 50, window: month }
ai_tokens: { limit: 100_000, window: month }
pdf_export: false
pro:
ai_chat: { limit: 5_000, window: month, on_limit: degrade } # block | degrade | bill
ai_tokens: { limit: 10_000_000, window: month }
pdf_export: true
enterprise:
ai_chat: unlimited
ai_tokens: { limit: 100_000_000, window: month, on_limit: bill, overage_price: 0.50/1_000_000 }
pdf_export: true
on_limit policies:
- block — deny the request (default)
- degrade — allow it, but flag
res.degraded = Trueso you can route to a cheaper model - bill — allow it and emit an overage event your billing system can invoice
Token-aware AI helpers
The part everyone gets wrong. Cupo ships thin wrappers around the Anthropic and OpenAI clients that meter tokens automatically — including streaming, where usage is only known when the stream ends:
from cupo.anthropic import metered
client = metered(anthropic.Anthropic(), cupo, feature="ai_tokens")
# streaming: tokens are tracked when the stream closes, with the request's idempotency key
with client.messages.stream(model="claude-sonnet-4-6", ...) as stream:
for text in stream.text_stream:
yield text
How it works
your app ──▶ Cupo SDK ──▶ counters (your Postgres, or Redis, or Cupo server)
│
├─ local entitlement cache (checks add ~0 network latency)
└─ async usage flush (batched, idempotent)
Three problems Cupo solves so you don't have to:
- Atomicity. Two concurrent requests with one credit left: exactly one passes. Counters use atomic operations (
INCR/ row-level locks), never read-modify-write. - Latency. Entitlements are cached in-process and synced in the background. A
check()is a dictionary lookup, not a network call. Trade-off: near the limit, a small overshoot is possible — this is configurable (strict: trueforces a synchronous check for expensive features). - Idempotency. Every
track()takes an idempotency key. Retries, at-least-once queues, and network flakiness never double-count.
Failure mode is yours to choose: fail_open (if Cupo is unreachable, allow the request — default, your product stays up) or fail_closed (deny — for features where overshoot costs you real money).
Deployment modes
| Mode | What it needs | For |
|---|---|---|
| Embedded | Your existing Postgres (Supabase works) | Solo devs, single service |
| Server | Docker container + Redis | Multiple services / languages |
| Cloud (planned) | Nothing — hosted | Teams that want dashboards, analytics, SLA |
Webhooks
The server emits events so you can warn customers before they hit the wall:
usage.threshold— configurable (e.g. at 80% of any limit)usage.limit_reachedusage.overage— with units and computed price, ready to forward to your billing
What Cupo is not
- Not a billing platform. It doesn't generate invoices or charge cards. It pairs with Stripe, Mercado Pago, Lago, or whatever you already use (plan sync integrations are on the roadmap).
- Not an API gateway. It runs inside your app, not in front of it.
- Not for enterprise contract management. If you have negotiated multi-year commits with drawdowns, you want Metronome or Orb.
FAQ
vs. Lago / Metronome / Orb? Those meter usage to bill it. Cupo meters usage to enforce it, in the request path, in real time. Different layer — Cupo can feed them.
vs. Stigg? Closest neighbor. Stigg is a hosted, dashboard-first entitlements platform aimed at teams. Cupo is open source, config-as-code, and designed so a solo developer is enforcing limits 15 minutes after pip install cupo.
vs. rolling my own? You can. Most people's version has the concurrency bug, misses streaming tokens, and has no idempotency. Ours has tests for all three.
Why should I trust the counters? Every counter mutation is an atomic operation with tests that hammer it concurrently. The test suite is part of the pitch — read it.
Roadmap
- v0.1 — Python SDK, embedded mode (Postgres), plans-as-code, FastAPI middleware, Anthropic/OpenAI metered wrappers, docs + 3 runnable examples
- v0.2 — Standalone server (Docker), TypeScript SDK, webhooks, Redis counters
- v0.3 — Stripe & Mercado Pago plan sync, usage dashboard, hosted cloud (free tier + flat self-serve pricing — no "talk to sales")
License
SDKs: MIT. Server: AGPL-3.0. Self-hosting is free forever; the hosted cloud is how the project sustains itself.
Would you use this? Open an issue titled feedback: and tell us — especially if the answer is no and why. If you've hand-rolled this layer before, we'd love 20 minutes of your war stories.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cupo-0.0.1.tar.gz.
File metadata
- Download URL: cupo-0.0.1.tar.gz
- Upload date:
- Size: 5.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7138c3d9b4dd72ec342c5399c5a40063ecc7ea157d878ef99731eba08ccf92aa
|
|
| MD5 |
5cc460d1dd476e5beb1c5c02fa27ee09
|
|
| BLAKE2b-256 |
621e3f936e9a45d8e890fa7b037780ae0bd5c1b7c5309c42867571204e894df4
|
File details
Details for the file cupo-0.0.1-py3-none-any.whl.
File metadata
- Download URL: cupo-0.0.1-py3-none-any.whl
- Upload date:
- Size: 5.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c9497f6915b6c2d3fcd7d13f805b240ff0dff9dfb8336b593b9e1f08d5e9c92
|
|
| MD5 |
14789ab3e0f4d1a6f8b7fdb2dd1e270a
|
|
| BLAKE2b-256 |
92fe8aa9076ccc5eaf826e5e8c7c827852d5698f27f5a4fa3b8883c2c3d87ae3
|