Recipes
Battle-tested patterns. Copy, paste, adapt.
Next.js App Router - protected AI endpoint
// lib/vevee.ts
import { createClient } from '@vevee/sdk';
export const vevee = createClient({ apiKey: process.env.VEVEE_KEY! });
// app/api/generate/route.ts
import { vevee } from '@/lib/vevee';
import { auth } from '@/lib/auth';
import { VeveeError } from '@vevee/sdk';
export async function POST(req: Request) {
const session = await auth();
if (!session?.userId) return new Response('unauthorized', { status: 401 });
const { prompt } = await req.json();
let reservationId: string | undefined;
try {
const r = await vevee.reserve(session.userId, 'image.render', 1, { model: 'flux-pro' });
if (!r.allowed) {
return Response.json({ error: 'limit_reached', reasons: r.reasons }, { status: 429 });
}
reservationId = r.reservationId;
const image = await callFluxPro(prompt);
await vevee.commit(reservationId!);
return Response.json({ image });
} catch (err) {
if (reservationId) await vevee.release(reservationId).catch(() => {});
if (err instanceof VeveeError) {
return Response.json({ error: err.code }, { status: err.status });
}
throw err;
}
}Express middleware - gate any route by limit
import { vevee } from './vevee';
export function requireQuota(event: string, quantity = 1) {
return async (req, res, next) => {
const ok = await vevee.can(req.user.id, event, quantity);
if (!ok) return res.status(429).json({ error: 'limit_reached' });
next();
};
}
app.post('/api/summarize', requireQuota('llm.completion', 1000), summarizeHandler);Streaming LLM - track real token count after the stream ends
// Reserve a generous upper bound; commit with the actual count after.
const r = await vevee.reserve(userId, 'llm.completion', 4000, { model: 'gpt-4o' });
if (!r.allowed) throw new Error('limit_reached');
let realTokens = 0;
try {
const stream = await openai.chat.completions.create({ ... , stream: true });
for await (const chunk of stream) {
realTokens += chunk.usage?.total_tokens ?? 0;
yield chunk;
}
await vevee.commit(r.reservationId!);
// Optional: record the difference as a separate corrective event
if (realTokens < 4000) {
await vevee.track(userId, 'llm.completion.refund', 4000 - realTokens, { model: 'gpt-4o' });
}
} catch (err) {
// Capture why so you can later debug provider failures vs user cancels.
await vevee.release(r.reservationId!, {
errorCode: 'provider_error',
reason: err instanceof Error ? err.message : String(err),
});
throw err;
}Multi-provider fallback - only charge once
const r = await vevee.reserve(userId, 'image.render', 1);
if (!r.allowed) throw new Error('limit_reached');
try {
const img = await tryFlux().catch(() => trySDXL()).catch(() => tryDallE());
await vevee.commit(r.reservationId!);
return img;
} catch (err) {
await vevee.release(r.reservationId!, { errorCode: 'all_providers_failed' });
throw err;
}Onboarding - assign free plan on signup
// In your signup handler, after creating the user
await vevee.upsertSubscription({
userId: newUser.id,
planId: 'plan_free',
});Stripe webhook - sync paid plans
export async function POST(req: Request) {
const event = stripe.webhooks.constructEvent(/* … */);
switch (event.type) {
case 'checkout.session.completed': {
const s = event.data.object;
await vevee.upsertSubscription({
userId: s.client_reference_id!,
planId: planIdFromStripeProduct(s),
});
break;
}
case 'customer.subscription.deleted': {
await vevee.upsertSubscription({
userId: event.data.object.metadata.userId!,
planId: 'plan_free',
});
break;
}
}
return new Response('ok');
}React - show remaining quota in the UI
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@vevee/sdk';
const vevee = createClient({ apiKey: process.env.NEXT_PUBLIC_VEVEE_KEY! });
export function QuotaBadge({ userId }: { userId: string }) {
const [data, setData] = useState<{ remaining: number; quota: number; resetsAt: string | null } | null>(null);
useEffect(() => {
let cancelled = false;
(async () => {
const u = await vevee.usage(userId, 'image.render');
const c = u.counters[0];
if (!cancelled && c) {
// canUse() also returns details.quota, you could combine these.
setData({ remaining: 50 - c.count, quota: 50, resetsAt: u.period.end });
}
})();
return () => { cancelled = true; };
}, [userId]);
if (!data) return null;
return (
<div>
{data.remaining}/{data.quota} images
{data.resetsAt && <> · resets {new Date(data.resetsAt).toLocaleDateString()}</>}
</div>
);
}