Credit System Guide

Credits map model spend to something customers understand. The tables and cron hooks here track balances, burns per request, and plan resets.

Overview

Credits give you:

  • Monetization: bill for tokens, not flat pages
  • Cost control: hard stop before API invoices surprise you
  • Plan tiers: different monthly buckets per Stripe price

How Credits Work

Credit Costs

Each AI model has a cost per 1,000 tokens:

ModelCost per 1k Tokens
GPT-4o Mini1 credit
GPT-4o5 credits
GPT-4 Turbo10 credits
Claude 3.5 Sonnet10 credits
Claude 3 Opus15 credits

Cost Calculation

// Formula
cost = Math.ceil((inputTokens + outputTokens) / 1000) * model.costPer1kTokens

// Example: 500 input + 800 output = 1300 tokens
// With GPT-4o Mini (1 credit/1k): ceil(1300/1000) × 1 = 2 credits

Tier Allocations

Free Tier

  • 100 credits/month
  • Access to: GPT-4o Mini only
  • Features: Basic chat

Pro Tier

  • 2,500 credits/month
  • Access to: GPT-4o Mini, GPT-4o, Claude 3.5 Sonnet
  • Features: RAG, multi-model, tool use, priority support

Enterprise Tier

  • 10,000 credits/month
  • Access to: All models
  • Features: All features + team management

API Flow

Pre-Check (Estimation)

Before calling the AI:

// Estimate cost based on input length
const estimatedCost = estimateCreditCost(
  selectedModel,
  estimatedInputTokens,
  maxOutputTokens
);

// Return 402 if insufficient
if (profile.credits < estimatedCost) {
  return new Response("Insufficient Credits", { status: 402 });
}

Post-Check (Actual)

After AI response completes:

// In onFinish callback
const actualCost = calculateCreditCost(selectedModel, {
  promptTokens: usage.promptTokens,
  completionTokens: usage.completionTokens,
});

// Deduct credits
await supabase
  .from("profiles")
  .update({ credits: profile.credits - actualCost })
  .eq("id", user.id);

Configuration

Model Costs (src/config/ai-models.ts)

export const AI_MODELS = {
  "gpt-4o-mini": {
    provider: "openai",
    costPer1kTokens: 1,
    // ...
  },
  "gpt-4o": {
    provider: "openai",
    costPer1kTokens: 5,
    // ...
  },
};

Tier Credits (src/config/pricing.ts)

export const PRICING_TIERS = {
  free: {
    monthlyCredits: 100,
    creditReset: "monthly",
    // ...
  },
  pro: {
    monthlyCredits: 2500,
    creditReset: "monthly",
    // ...
  },
};

Credit Utilities

estimateCreditCost

Pre-flight estimation:

import { estimateCreditCost } from "@/lib/ai/credit-calculator";

const cost = estimateCreditCost("gpt-4o-mini", 500, 1000);
// Returns: 2 (for 1500 estimated tokens)

calculateCreditCost

Post-completion calculation:

import { calculateCreditCost } from "@/lib/ai/credit-calculator";

const cost = calculateCreditCost("gpt-4o-mini", {
  promptTokens: 500,
  completionTokens: 800,
});
// Returns: 2 (for 1300 actual tokens)

UI Components

Credit Widget

Displays in sidebar:

  • Current credit balance
  • Visual progress bar
  • Color coding (green/yellow/red)
  • Click to navigate to billing

Insufficient Credits Dialog

Shown when 402 error occurs:

  • Upgrade prompt
  • Link to billing page

Credit Reset

Credits reset based on tier configuration:

creditReset: "monthly"  // Resets on billing cycle
creditReset: "never"    // Credits accumulate (top-ups)

Top-Up System

One-time credit purchases:

  1. User clicks "Top Up" on billing page
  2. Redirects to Stripe checkout (one-time payment)
  3. Webhook adds credits to existing balance
// In webhook handler
if (type === "one-time") {
  const creditsToAdd = 1000;
  await supabase
    .from("profiles")
    .update({ credits: profile.credits + creditsToAdd })
    .eq("id", userId);
}

Monitoring Usage

Usage Chart

The billing page shows a 30-day usage chart:

  • Credits spent per day
  • Uses Recharts for visualization
  • Data from messages.tokens_used

Database Query

SELECT
  DATE(created_at) as date,
  SUM(tokens_used) as total_tokens
FROM messages
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY DATE(created_at)
ORDER BY date;