How to Implement Credit-Based Generation with Super Admin Forced Charging in a GPT Image Application
The website enforces credit-based image generation through a multi-layered architecture combining API-level conditional routing, Supabase RPC calls, and SQL stored procedures that deduct credits atomically, with Super-Admins bypassing free-generation quotas via a p_force_credit flag that guarantees credit consumption on every request.
The freestylefly/awesome-gpt-image-2 repository demonstrates a production-ready billing system where image generation is gated by credit validation. This guide breaks down how the codebase coordinates authentication, reservation logic, and SQL-level business rules to ensure accurate credit accounting—with special handling for Super-Admin users who must always pay despite any free-tier entitlements.
Authentication and Profile Loading for Credit Checks
Every generation request begins with identity verification and profile hydration. In api/_lib/supabase.js, the getAuthContext function validates the bearer token and constructs a profile containing three critical billing fields:
creditBalance— current purchasable creditsfreeGenerationsUsed— consumption of promotional quotaisSuperAdmin— boolean derived fromrow.role === 'super_admin'
// api/_lib/supabase.js lines 49-55
const profile = {
email: row.email,
isSuperAdmin: row.role === 'super_admin',
creditBalance: row.credit_balance,
freeGenerationsUsed: row.free_generations_used,
// ...
};
This profile drives all downstream charging decisions. The isSuperAdmin flag is not merely presentational—it directly controls which reservation path the API invokes.
API-Level Reservation Routing
The core enforcement logic resides in api/generate-image.js. The handler inspects auth.profile.isSuperAdmin to select between two reservation functions:
// api/generate-image.js
reservation = auth.profile.isSuperAdmin
? await reserveCreditGeneration(auth.client, auth.user.id, caseId, prompt)
: await reserveGeneration(auth.client, auth.user.id, caseId, prompt);
Regular users trigger reserveGeneration, which calls the Supabase RPC reserve_generation_usage with p_force_credit: false (line 60). Super-Admins invoke reserveCreditGeneration with p_force_credit: true (line 84). This single boolean parameter cascades into dramatically different SQL behavior.
SQL Reservation Logic with Force-Credit Override
The stored procedure reserve_generation_usage lives in supabase/migrations/20260509090000_membership_billing.sql. Its execution flow branches on both user status and the p_force_credit parameter:
| Condition | Action | Credit Consumed |
|---|---|---|
free_generations_used < 1 AND p_force_credit = false |
Create free reservation, increment free_generations_used |
0 |
credit_balance >= 1 |
Atomic credit deduction, create reservation, log transaction | 1 |
| Neither condition met | Raise CREDITS_REQUIRED exception |
N/A |
The Super-Admin forced charging mechanism works through p_force_credit = true. When this flag is set, the procedure skips the free-generation branch entirely and proceeds directly to credit deduction. Even if a Super-Admin has unused free generations, they are charged one credit per image.
Lines 70-73 handle the failure case for insufficient credits:
-- supabase/migrations/20260509090000_membership_billing.sql
RAISE EXCEPTION 'CREDITS_REQUIRED' USING errcode = 'P0001';
This exception propagates to the API layer, which translates it into an HTTP 402 CREDITS_REQUIRED response (lines 10-12 of generate-image.js). Other RPC errors become generic GENERATION_FAILED messages after logging (lines 14-17).
Post-Generation Credit Accounting
After successful image generation, the system finalizes the reservation:
// From the analysis: completeReservation marks reservation completed (lines 5-9)
await completeReservation(reservation.id, generationData);
The updated profile—including the decremented creditBalance—returns to the client via profileFromReservation (lines 44-55), ensuring UI synchronization without requiring a separate profile fetch.
Frontend Integration and Error Handling
The React frontend in src/main.jsx (lines 822-894) interprets these responses to control the generation interface:
- Displays credit balance and free-generation status
- Disables generate button on
CREDITS_REQUIRED(402) - Shows Super-Admin indicators when
profile.isSuperAdminis true
This creates a cohesive user experience where billing constraints are enforced server-side but communicated clearly client-side.
Complete Implementation Examples
Regular User: Free-Then-Paid Flow
// api/generate-image.js
reservation = await reserveGeneration(client, userId, caseId, prompt);
// Internally invokes:
client.rpc('reserve_generation_usage', {
p_user_id: userId,
p_case_id: caseId,
p_prompt: prompt,
p_force_credit: false // allows free generation if available
});
Super-Admin: Forced Credit Deduction
// api/generate-image.js
reservation = await reserveCreditGeneration(client, userId, caseId, prompt);
// Internally invokes:
client.rpc('reserve_generation_usage', {
p_user_id: userId,
p_case_id: caseId,
p_prompt: prompt,
p_force_credit: true // skips free tier, always charges
});
SQL Credit Deduction and Transaction Logging
-- Simplified from reserve_generation_usage
IF v_profile.credit_balance >= 1 THEN
UPDATE public.profiles
SET credit_balance = credit_balance - 1
WHERE id = p_user_id
RETURNING * INTO v_profile;
INSERT INTO public.generation_reservations
(user_id, case_id, prompt, force_credit, credit_amount, usage_source)
VALUES
(p_user_id, p_case_id, p_prompt, p_force_credit, 1, 'credit');
INSERT INTO public.credit_transactions
(user_id, amount, type, source, reference_id, metadata)
VALUES
(p_user_id, -1, 'generation', 'case_generation_test', v_reservation_id,
jsonb_build_object('caseId', p_case_id, 'usageSource', 'credit'));
END IF;
Architecture Summary
| Layer | Component | Billing Responsibility |
|---|---|---|
| Frontend | src/main.jsx |
Display credit state, handle 402 errors |
| API Gateway | api/generate-image.js |
Route to reserveGeneration or reserveCreditGeneration based on isSuperAdmin |
| Auth Utility | api/_lib/supabase.js |
Hydrate profile with creditBalance, freeGenerationsUsed, isSuperAdmin |
| Database RPC | reserve_generation_usage |
Atomic credit check, deduction, reservation creation, transaction logging |
| Migration | 20260509090000_membership_billing.sql |
Schema and procedure definitions, CREDITS_REQUIRED exception |
Summary
- Authentication layer embeds billing state (
creditBalance,isSuperAdmin) into every request context viagetAuthContext - API handler branches on
profile.isSuperAdminto setp_force_credittrue or false - SQL procedure atomically implements: free-tier check, credit deduction, transaction logging, and insufficient-credit errors
- Super-Admin forced charging bypasses free-generation logic entirely through the
p_force_creditparameter - Error propagation converts SQL
CREDITS_REQUIREDinto HTTP 402 for client handling - Profile synchronization returns updated balances post-generation without extra round-trips
Frequently Asked Questions
What happens when a user has both free generations and credits available?
The SQL procedure checks p_force_credit first. For regular users (p_force_credit: false), it consumes the free generation before touching paid credits. For Super-Admins (p_force_credit: true), the free-generation branch is skipped and one credit is deducted immediately, regardless of free-generation status.
How does the system prevent race conditions in credit deduction?
The reserve_generation_usage stored procedure executes within a single database transaction. The UPDATE ... WHERE credit_balance >= 1 pattern ensures atomic deduction—concurrent requests for the last credit will serialize, with all but one failing the balance check and raising CREDITS_REQUIRED.
Can the forced-credit mechanism be extended to other user roles?
Yes. The p_force_credit parameter is role-agnostic. To force charging for additional roles, modify the conditional in api/generate-image.js to check profile.role against an array (e.g., ['super_admin', 'premium_tier']) rather than only isSuperAdmin. No SQL changes are required.
Why return HTTP 402 instead of 403 or 400 for insufficient credits?
HTTP 402 Payment Required is the semantically correct status for credit exhaustion. It distinguishes billing failures from authentication (401/403) or validation errors (400), allowing clients to trigger upsell flows precisely when users need to purchase credits.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →