Credits & Consumption
The credits system is used in Credits mode and Quota Subscription mode. If you're using Unlimited Subscription or Lifetime mode, you can skip this page.
Three Types of Credits
The system maintains three types of credits, consumed in priority order:
| Type | Source | Expiration | Consumption Priority |
|---|---|---|---|
subscription_credits | Monthly quota from quota subscription | Resets monthly | Highest |
bonus_credits | Gifted / promotional credits | Has expiry date | Medium |
purchased_credits | Credits purchased by the user | Never expires | Lowest |
Consumption order: Monthly quota first → then bonus credits → then purchased credits.
This priority is the most user-friendly — use the ones expiring soonest first, save purchased credits for last.
useCredits Hook
import { useCredits } from '@/hooks/useCredits'
function CreditsDisplay() {
const { balance, loading, refreshBalance, useCredit } = useCredits()
if (loading) return <div>Loading...</div>
return (
<div>
<p>Available credits: {balance.availableCredits}</p>
<p>Purchased credits: {balance.purchasedCredits}</p>
<p>Used: {balance.usedCredits}</p>
</div>
)
}balance Object
{
totalCredits: number, // Total credits ever received
usedCredits: number, // Credits used
availableCredits: number, // Currently available credits
purchasedCredits: number, // Purchased credits balance
}useCredit — Consume Credits
const { useCredit } = useCredits()
async function handleUse() {
const result = await useCredit(
'image_generation', // Service type
'Generated an image', // Description (optional)
'img-456' // Related ID (optional)
)
if (result.success) {
console.log('Remaining:', result.remainingCredits)
} else {
// Insufficient credits
console.log(result.message)
}
}useCredit internally calls /api/service/use, which shares the same backend endpoint as useService from useAccess. Both can be used — the difference is:
useService(fromuseAccess) — General-purpose consumption, automatically updates access statususeCredit(fromuseCredits) — Credits-focused, automatically updates the credits balance display
Refreshing Balance
After a successful purchase or when you need to manually refresh the credits balance:
const { refreshBalance, addCredits } = useCredits()
// Method 1: Re-fetch from server (accurate)
await refreshBalance()
// Method 2: Locally increment (instant UI update)
addCredits(50) // Local balance +50Server-Side Credit Operations
In API Routes or server-side logic, you can use the functions provided by lib/payment/credits.ts:
import {
addCreditsToUser, // Add purchased credits
addBonusCredits, // Add bonus credits (with expiry)
getTotalAvailableCredits, // Get total available credits
initSubscriptionCredits, // Initialize subscription quota credits
clearSubscriptionCredits, // Clear subscription quota credits (on cancellation)
} from '@/lib/payment/credits'
// Add 50 purchased credits to a user
await addCreditsToUser(userId, 50)
// Grant 10 bonus credits, expiring in 30 days
import { getBonusExpiryDate } from '@/config/payment'
await addBonusCredits(userId, 10, getBonusExpiryDate(30))
// Query total available credits
const total = await getTotalAvailableCredits(userId)Bonus Credits
Bonus credits (bonus_credits) are managed in batches, each with its own expiry date. Suitable for sign-up rewards, promotional campaigns, subscription perks, etc.
The "Grant 10 Bonus" button in the homepage Hero section (
components/landing/Hero.tsx) is a complete frontend example of granting bonus credits.
Configure expiry days:
Set the default expiry days in config/payment.ts:
export const BONUS_CREDITS_EXPIRY_DAYS = 30 // Default 30 daysAutomatic granting scenarios:
- When a subscription is activated, if
bonus_creditsis configured in the product metadata, bonus credits are automatically granted - When a credits package is purchased, if
bonus_creditsis configured in the metadata, bonus credits are included with the purchase
Manual granting (frontend):
const { grantBonus } = useAccess()
// Grant 10 credits, default 30-day expiry
const res = await grantBonus(10)
// Grant 50 credits, 7-day expiry
const res = await grantBonus(50, 7)
// res.success / res.messageManual granting (backend API route / server action):
import { addBonusCredits } from '@/lib/payment/credits'
import { getBonusExpiryDate } from '@/config/payment'
// Grant 20 credits, expiring in 30 days
await addBonusCredits(userId, 20, getBonusExpiryDate(30))
// Grant 50 credits, expiring in 7 days, tagged as campaign
await addBonusCredits(userId, 50, getBonusExpiryDate(7), 'campaign')Expiry mechanism: A database cron job scans and marks expired batches every 10 minutes. A trigger automatically recalculates the user_credits balance. The application layer also performs a fallback check when the user accesses the service.
Atomic Deduction
All credit deduction operations are executed atomically via the database RPC consume_quota, eliminating any risk of over-deduction due to concurrency. The RPC internally deducts in priority order (subscription_credits → bonus_credits → purchased_credits) and automatically logs consumption records.