Manage the credit system (allocation, purchasing, usage). Use when adding credit types, configuring pricing, or building credit UI.
This skill guides you through the entire credit system, from backend configuration to frontend UI implementation.
All credit configuration lives in src/lib/credits/config.ts.
Define the Type: Add the new type to creditTypeSchema enum.
export const creditTypeSchema = z.enum([
"image_generation",
"video_generation",
"your_new_credit_type" // Add this
]);
Configure Pricing & Metadata: Add an entry to creditsConfig.
your_new_credit_type: {
name: "New Credit Name",
currency: "USD",
minimumAmount: 10,
// Option A: Fixed Slabs
slabs: [
{ from: 1, to: 100, pricePerUnit: 0.10 },
{ from: 101, to: 1000, pricePerUnit: 0.08 },
],
// Option B: Dynamic Calculator (e.g., based on user plan)
priceCalculator: (amount, userPlan) => {
// Logic here
return amount * 0.1;
}
}
Plan Allocations (Optional): Define how many credits are given when subscribing to a plan in onPlanChangeCredits.
To let users purchase credits, use the useBuyCredits hook. This hook handles price calculation (factoring in plan discounts) and generating checkout URLs.
useBuyCreditsLocation: src/lib/credits/useBuyCredits.ts
import useBuyCredits from "@/lib/credits/useBuyCredits";
import { PlanProvider } from "@/lib/plans/getSubscribeUrl";
const {
price, // Calculated total price (number | undefined)
isLoading, // Price calculation in progress
error, // Error state
getBuyCreditsUrl // Function to generate payment URL
} = useBuyCredits(creditType, amount);
Here is a pattern for creating a credit purchase UI, similar to src/components/website/website-credits-section.tsx.
"use client";
import { useState } from "react";
import useBuyCredits from "@/lib/credits/useBuyCredits";
import { PlanProvider } from "@/lib/plans/getSubscribeUrl";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
// 1. Define your packages
const PACKAGE = {
credits: 100,
name: "Starter Pack",
};
export function BuyCreditsCard({ creditType }: { creditType: "image_generation" }) {
const [provider] = useState(PlanProvider.STRIPE); // or LEMONSQUEEZY
// 2. Call the hook
const { price, isLoading, error, getBuyCreditsUrl } = useBuyCredits(
creditType,
PACKAGE.credits
);
// 3. Handle Purchase
const handleBuy = () => {
const url = getBuyCreditsUrl(provider);
window.location.href = url;
};
// 4. Render UI
return (
<div className="border p-4 rounded-lg">
<h3>{PACKAGE.name}</h3>
<div className="text-2xl font-bold">
{isLoading ? (
<Loader2 className="animate-spin" />
) : (
`$${price?.toFixed(2) || "0.00"}`
)}
</div>
<Button
onClick={handleBuy}
disabled={isLoading || !price}
className="w-full mt-4"
>
Buy {PACKAGE.credits} Credits
</Button>
{error && <p className="text-red-500 text-sm">{error.message}</p>}
</div>
);
}
To show the user's current balance, use the useCredits hook.
useCreditsLocation: src/lib/users/useCredits.ts
const {
credits, // Record<string, number> | undefined
// e.g. { "image_generation": 100, "video_generation": 50 }
isLoading, // boolean
error, // any
mutate // SWR mutate function to refresh data
} = useCredits();
import { useCredits } from "@/hooks/use-current-user";
export function CreditBalance() {
const { credits, isLoading } = useCredits();
if (isLoading) return <div>Loading...</div>;
return (
<div>
Image Credits: {credits?.image_generation || 0}
</div>
);
}
These functions are used in API routes and webhooks.
Use allocatePlanCredits to give users credits when they subscribe/upgrade.
src/lib/credits/allocatePlanCredits.tsuserId, planId, paymentId (for idempotency).onPlanChangeCredits config and adds credits if applicable.To manually manipulate balances, use helpers from src/lib/credits/recalculate.ts (e.g., addCredits, deductCredits).
Note: Always ensure you have a unique paymentId or transaction reference when adding credits to prevent duplicates.
Use canDeductCredits before performing an action.
import { canDeductCredits } from "@/lib/credits/credits";
// Check if user has enough credits
const hasBalance = canDeductCredits(
"image_generation",
1,
user // Must contain { credits: { ... } }
);
if (!hasBalance) {
throw new Error("Insufficient credits");
}
For deep dives into database schema and architecture, see reference.md.