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>
  );
}