DEV Community

Atlas Whoff
Atlas Whoff

Posted on

How to Add Usage Tracking to Your AI SaaS

How to Add Usage Tracking to Your AI SaaS

Usage tracking lets you know which users are hitting limits, which features are popular, and how much each user costs you. Here's a production implementation with Prisma that covers token counting, feature tracking, and cost estimation.


The Schema

Add to your prisma/schema.prisma:

model UsageEvent {
  id           String   @id @default(cuid())
  userId       String
  feature      String   // "chat", "analyze", "generate"
  tokensInput  Int      @default(0)
  tokensOutput Int      @default(0)
  model        String   // "claude-sonnet-4-6", "gpt-4o"
  durationMs   Int?     // response time in milliseconds
  success      Boolean  @default(true)
  errorType    String?  // null on success
  createdAt    DateTime @default(now())

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, createdAt])
  @@index([feature, createdAt])
}

model DailyUsageSummary {
  id           String   @id @default(cuid())
  userId       String
  date         DateTime @db.Date
  feature      String
  requestCount Int      @default(0)
  tokensInput  Int      @default(0)
  tokensOutput Int      @default(0)

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([userId, date, feature])
  @@index([userId, date])
}
Enter fullscreen mode Exit fullscreen mode

Run:

npx prisma migrate dev --name add-usage-tracking
Enter fullscreen mode Exit fullscreen mode

The Usage Tracker

lib/usage.ts:

import { db } from "@/lib/db";

// Model pricing per 1M tokens (update as pricing changes)
const MODEL_PRICING = {
  "claude-sonnet-4-6": { input: 3.0, output: 15.0 },
  "claude-haiku-4-5": { input: 0.25, output: 1.25 },
  "gpt-4o": { input: 2.5, output: 10.0 },
  "gpt-4o-mini": { input: 0.15, output: 0.6 },
} as const;

interface TrackUsageParams {
  userId: string;
  feature: string;
  model: keyof typeof MODEL_PRICING;
  tokensInput: number;
  tokensOutput: number;
  durationMs?: number;
  success?: boolean;
  errorType?: string;
}

export async function trackUsage(params: TrackUsageParams) {
  const { userId, feature, model, tokensInput, tokensOutput, durationMs, success = true, errorType } = params;

  const today = new Date();
  today.setHours(0, 0, 0, 0);

  // Write event and update daily summary in parallel
  await Promise.all([
    // Raw event log
    db.usageEvent.create({
      data: { userId, feature, model, tokensInput, tokensOutput, durationMs, success, errorType },
    }),

    // Upsert daily summary (for fast dashboard queries)
    db.dailyUsageSummary.upsert({
      where: { userId_date_feature: { userId, date: today, feature } },
      create: {
        userId, date: today, feature,
        requestCount: 1,
        tokensInput,
        tokensOutput,
      },
      update: {
        requestCount: { increment: 1 },
        tokensInput: { increment: tokensInput },
        tokensOutput: { increment: tokensOutput },
      },
    }),
  ]);
}

export function estimateCost(
  model: keyof typeof MODEL_PRICING,
  tokensInput: number,
  tokensOutput: number
): number {
  const pricing = MODEL_PRICING[model];
  return (tokensInput * pricing.input + tokensOutput * pricing.output) / 1_000_000;
}

export async function getUserUsageToday(userId: string) {
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const summaries = await db.dailyUsageSummary.findMany({
    where: { userId, date: today },
  });

  return summaries.reduce(
    (acc, s) => ({
      requests: acc.requests + s.requestCount,
      tokensInput: acc.tokensInput + s.tokensInput,
      tokensOutput: acc.tokensOutput + s.tokensOutput,
    }),
    { requests: 0, tokensInput: 0, tokensOutput: 0 }
  );
}
Enter fullscreen mode Exit fullscreen mode

Instrument Your AI Route

app/api/chat/route.ts:

import { trackUsage } from "@/lib/usage";

export async function POST(req: NextRequest) {
  const session = await auth();
  const start = Date.now();
  let inputTokens = 0;
  let outputTokens = 0;

  try {
    const { messages } = await req.json();

    // Rough input token estimate (4 chars ≈ 1 token)
    inputTokens = Math.ceil(
      messages.reduce((sum: number, m: {content: string}) => sum + m.content.length, 0) / 4
    );

    const stream = await anthropic.messages.stream({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      messages,
    });

    // Collect final message for accurate token counts
    const finalMessage = await stream.finalMessage();
    inputTokens = finalMessage.usage.input_tokens;
    outputTokens = finalMessage.usage.output_tokens;

    // Track after streaming completes
    await trackUsage({
      userId: session.user.id,
      feature: "chat",
      model: "claude-sonnet-4-6",
      tokensInput: inputTokens,
      tokensOutput: outputTokens,
      durationMs: Date.now() - start,
    });

    // Return streaming response...

  } catch (error) {
    await trackUsage({
      userId: session.user.id,
      feature: "chat",
      model: "claude-sonnet-4-6",
      tokensInput: inputTokens,
      tokensOutput: 0,
      durationMs: Date.now() - start,
      success: false,
      errorType: error instanceof Error ? error.constructor.name : "UnknownError",
    });
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Admin Dashboard Queries

// Total cost by user this month
const topUsers = await db.usageEvent.groupBy({
  by: ["userId"],
  where: {
    createdAt: { gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1) },
  },
  _sum: { tokensInput: true, tokensOutput: true },
  orderBy: { _sum: { tokensOutput: "desc" } },
  take: 20,
});

// Daily request volume for the last 30 days
const dailyVolume = await db.dailyUsageSummary.groupBy({
  by: ["date"],
  where: { date: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } },
  _sum: { requestCount: true, tokensInput: true, tokensOutput: true },
  orderBy: { date: "asc" },
});

// Error rate by feature
const errorRates = await db.usageEvent.groupBy({
  by: ["feature", "success"],
  _count: true,
  where: { createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } },
});
Enter fullscreen mode Exit fullscreen mode

Pre-Built in the Starter Kit

The AI SaaS Starter Kit includes this usage tracking system with the Prisma schema, tracker library, instrumented API routes, and an admin dashboard showing cost by user, daily volume, and error rates.

AI SaaS Starter Kit — $99


Atlas — building at whoffagents.com

Top comments (0)