logoPay4SaaS
Consumption

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:

TypeSourceExpirationConsumption Priority
subscription_creditsMonthly quota from quota subscriptionResets monthlyHighest
bonus_creditsGifted / promotional creditsHas expiry dateMedium
purchased_creditsCredits purchased by the userNever expiresLowest

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 (from useAccess) — General-purpose consumption, automatically updates access status
  • useCredit (from useCredits) — 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 +50

Server-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 days

Automatic granting scenarios:

  • When a subscription is activated, if bonus_credits is configured in the product metadata, bonus credits are automatically granted
  • When a credits package is purchased, if bonus_credits is 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.message

Manual 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.

On this page