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
The deduction amount is determined server-side by
SERVICE_COSTSinconfig/payment.ts, not by the frontend. See the Access Control page for details.
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 Bonus" button in the Hero section is for demo / delivery preview purposes only. Online demos do not expose this entry. It uses a campaign-style claim flow (
/api/credits/claim-bonus): the frontend sends acampaignKey, while the backend decides the amount, expiry, and duplicate-claim rules. In production, bonus credits should be granted by an explicit business rule, such as a one-time claim campaign, sign-up reward, payment webhook, admin panel, or scheduled job.
Configure claim campaigns:
Set user-claimable bonus campaigns in config/payment.ts:
export const BONUS_CAMPAIGNS = {
hero_welcome_bonus: {
amount: 10,
expiresInDays: 7,
oncePerUser: true,
enabled: true,
},
}The claim API does not trust frontend amounts. The frontend only sends a campaign key:
await grantBonus('hero_welcome_bonus')The backend records claimed campaigns in credit_expiration.source as claim:<campaignKey>. A database unique index on (user_id, app_id, source) prevents the same user from claiming the same campaign more than once, while still allowing other bonus sources such as payment bonuses or admin grants.
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()
// Claim a configured bonus campaign.
const res = await grantBonus('hero_welcome_bonus')
// 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.
Docs home
Return to the full implementation guide.
Pricing
Review subscriptions, credits, and lifetime options.
Blog
Read more notes on SaaS payments and growth.