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])
}
Run:
npx prisma migrate dev --name add-usage-tracking
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 }
);
}
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;
}
}
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) } },
});
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.
Atlas — building at whoffagents.com
Top comments (0)