Add usage limits to your AI app in 10 minutes (no backend required)
You shipped an AI feature on Friday. By Monday one user had burned $212 of OpenAI credit on your free tier. The fix is not a TODO comment that says "add rate limiting" - it is two method calls.
The $212 user
Every AI product gets one. You launch a free tier with "10 generations a day" written in the pricing table and nowhere in the code, because enforcing it meant building a counters table, a period-rollover job, and an admin view - and you wanted to ship. Then one user writes a loop. The bill arrives before the limit does. The reason this keeps happening across thousands of indie AI apps is that usage limits feel like a feature you can defer, when they are actually the part of the product that touches money on both sides: your provider bill and your customer’s plan.
What "10 minutes" actually includes
This is the whole integration: create an app in the Vevee dashboard, define a plan with one limit group ("generations", 10 per day), copy the secret key, and wrap your AI call in a check and a track. There is no schema to design, no migration to run, no cron to schedule. The counters, periods, and rollover live on the other side of the API. Your code asks one question - can this user do this right now? - and reports one fact afterward - they did it.
import { createClient } from '@vevee/sdk';
const vevee = createClient({ apiKey: process.env.VEVEE_SECRET_KEY! });
export async function generateImage(userId: string, prompt: string) {
if (!(await vevee.can(userId, 'image_generation'))) {
throw new Error('limit_reached'); // show your upgrade screen
}
const image = await openai.images.generate({ prompt });
await vevee.track(userId, 'image_generation');
return image;
}You name the events, not us
The event string is yours. image_generation, llm_tokens, video_seconds, pdf_export - Vevee does not ship a fixed vocabulary, because your product’s unit of value is not ours to guess. The plan you built in the dashboard has match rules that map event patterns to limit groups, so when you later add a second model you can track image.flux and image.sdxl separately and still have both count toward one "images" quota. Start with one event name today; the naming scheme can grow without a migration.
The user id is whatever you already have
You pass end-user ids as opaque strings - an email, a UUID, your internal user id. Vevee never authenticates your users and never needs to; it just keys counters on the string you provide. That means there is no OAuth dance, no user-sync job, no webhook to keep two user tables consistent. The id you have in scope at the call site is the id you use.
What you get for the two calls
The can/track pair buys more than a gate. Every tracked event lands in a per-user usage timeline you can chart in the dashboard, every counter knows its period and resets itself when the plan says so, and changing the free tier from 10 to 25 generations is an edit in the plan builder - not a deploy. When you add a paid plan next month, upsertSubscription moves a user onto it and the same two lines of integration code start enforcing the new numbers.
When you outgrow can + track
There is one honest caveat: can followed by track has a race. Two parallel requests can both pass the check before either records usage, so a determined user can overshoot a tight limit. For most free tiers that is fine - the overshoot is one or two events. When it stops being fine (expensive video models, strict credit packs), switch to reserve/commit/release, which holds quota atomically before the AI call runs. The upgrade is local: same SDK, same event names, three calls instead of two, only on the paths that need it.
const r = await vevee.reserve(userId, 'video_seconds', 30);
if (!r.allowed) throw new Error('limit_reached');
try {
const video = await renderVideo(prompt);
await vevee.commit(r.reservationId!);
return video;
} catch (e) {
await vevee.release(r.reservationId!);
throw e;
}Keep reading
The race condition in "if (usage < limit)" that is costing your AI app money
A user at nine of ten images opens six tabs and clicks generate in all of them. Every tab passes your limit check, every tab gets an image, and you pay for all six. The bug is one read-then-write - and your unit tests will never catch it.
MeteringHow to build a credits system for your AI app (ledger design, rollover, refunds)
A user emails: "I had 40 credits this morning and now I have 12, and I only made one image." If your balance is a column you mutated, you cannot answer that email. Here is the ledger design that can.