How the GPT-Image2 Platform Credit System Works: Technical Architecture Guide
The GPT-Image2 platform uses a reservation-based credit accounting model where credits are deducted only after successful image generation completion, with all transactions logged atomically in Supabase.
The credit system in freestylefly/awesome-gpt-image-2 is engineered for reliability and auditability. Built on PostgreSQL stored procedures in Supabase, it separates credit reservation from settlement, ensuring users never lose credits on failed generations. This article breaks down the complete architecture from database schema to API flows.
Credit System Architecture Overview
The platform implements four interconnected layers:
- Storage layer –
profiles.credit_balance,credit_packs, andcredit_transactionstables - Reservation layer –
generation_reservationswithpending/completed/failedstates - Settlement layer – RPC functions
reserve_generation_usage,complete_generation_reservation, andrelease_generation_reservation - API layer – Next.js endpoints that orchestrate the flow
All credit logic lives in Supabase migrations and serverless functions, making the system provider-agnostic and easily extensible.
Core Data Model
Profile Balances and Transaction History
User credit state is split between a fast lookup column and a detailed ledger:
| Component | Table | Purpose |
|---|---|---|
| Current balance | profiles.credit_balance |
Integer column for instant availability checks |
| Purchase records | credit_packs |
Tracks purchased credit bundles |
| Audit trail | credit_transactions |
Immutable log of all credit movements |
The credit_transactions table captures type values including purchase, membership_grant, generation, refund, and admin_adjustment. This design satisfies accounting requirements while keeping balance checks performant.
Source: Schema defined in [supabase/migrations/20260509090000_membership_billing.sql](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/supabase/migrations/20260509090000_membership_billing.sql)
How Generation Reservations Work
Step 1: Reserving Credit with reserve_generation_usage
When a user requests an image, the API calls the stored procedure reserve_generation_usage. This function implements the core reservation logic:
- Checks if
free_generations_used < 1→ uses free slot if available - Otherwise verifies
credit_balance >= 1 - Creates a
generation_reservationsrow with:status: 'pending'type: 'generation'(or'free'for complimentary uses)provider: 'apimart'
- Returns the
reservation_idfor tracking
-- Conceptual flow from migration 20260512090000_google_account_center.sql
SELECT reserve_generation_usage(
p_user_id := 'uuid',
p_case_id := null,
p_prompt := 'a red sports car',
p_force_credit := false
);
The p_force_credit parameter allows bypassing free generation eligibility—useful for premium features or admin overrides.
Source: supabase/migrations/20260512090000_google_account_center.sql#L3-L44
Step 2: External Provider Processing
With the reservation created, the API forwards the generation task to APImart:
// From api/generate-image.js
const { data: reservation, error } = await supabase.rpc('reserve_generation_usage', {
p_user_id: user.id,
p_case_id: caseId || null,
p_prompt: prompt,
p_force_credit: forceCredit || false
});
// Reservation.id becomes the task correlation ID
await apimartClient.createTask({
taskId: reservation.id,
prompt: prompt,
webhookUrl: `${BASE_URL}/api/generation/callback`
});
The credit remains undeducted during this phase—only the reservation exists.
Step 3: Settlement via Callback
When APImart completes (or fails) the task, it calls the platform's callback endpoint. The helper settlePlatformGeneration in api/_lib/generation.js orchestrates the final accounting:
// Simplified from api/_lib/generation.js
export async function settlePlatformGeneration(supabase, reservation, providerResult) {
const { status, resultUrl, costUsd } = parseProviderPayload(providerResult);
if (status === 'success') {
// Deduct credit permanently and log transaction
await supabase.rpc('complete_generation_reservation', {
p_reservation_id: reservation.id,
p_provider_result_url: resultUrl,
p_provider_cost_usd: costUsd
});
} else {
// Release hold—no credit deducted
await supabase.rpc('release_generation_reservation', {
p_reservation_id: reservation.id,
p_failure_reason: providerResult.errorMessage
});
}
}
Source: api/_lib/generation.js#L60-L84 (settlement helper); [api/generation/callback.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generation/callback.js) (endpoint)
Reservation Completion Functions
| Function | Action | Credit Impact |
|---|---|---|
complete_generation_reservation |
Marks success, stores result URL | Decrements credit_balance by 1, creates generation transaction |
release_generation_reservation |
Marks failed/cancelled | No balance change, reservation archived |
This atomic settlement pattern prevents credit loss from provider failures, network timeouts, or duplicate callbacks.
Source: supabase/migrations/20260512090000_google_account_center.sql#L70-L90
Admin Credit Management
Granting and Adjusting Credits
Administrators manipulate balances through the grant_user_credits function, used for:
- Purchases – Stripe webhook triggers positive adjustment
- Membership grants – Recurring subscription credits
- Manual adjustments – Support compensations or corrections
- Refunds – Negative adjustments for disputed charges
// From api/admin/credits/adjust.js
const { error } = await supabase.rpc('grant_user_credits', {
p_user_id: targetUserId,
p_amount: amount, // positive or negative
p_type: 'admin_adjustment', // or 'purchase', 'membership_grant', 'refund'
p_description: reason,
p_related_entity_type: 'admin_panel',
p_related_entity_id: adminId
});
Every grant creates a corresponding credit_transactions row, maintaining full auditability even for manual interventions.
Source: supabase/migrations/20260509090000_membership_billing.sql#L205 (function definition); [api/admin/credits/adjust.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/admin/credits/adjust.js) (endpoint)
End-to-End Generation Flow
Here's the complete sequence from user action to credit settlement:
- User submits prompt →
POST /api/generate-image - Reserve credit →
reserve_generation_usageRPC executes - Queue with provider → APImart task created with reservation ID
- Async processing → Image generates (seconds to minutes)
- Provider callback →
POST /api/generation/callback - Find reservation →
findPlatformGenerationhelper locates pending row - Settle outcome →
complete_generation_reservationorrelease_generation_reservation
// Complete API handler example: api/generate-image.js
import { supabase } from '../_lib/supabase.js';
export default async function handler(req, res) {
const { user } = await authenticateRequest(req);
const { prompt, caseId, forceCredit } = req.body;
// Atomic reservation
const { data: reservation, error } = await supabase.rpc('reserve_generation_usage', {
p_user_id: user.id,
p_case_id: caseId || null,
p_prompt: prompt,
p_force_credit: forceCredit || false
});
if (error) {
if (error.message.includes('insufficient_credits')) {
return res.status(402).json({ error: 'Insufficient credits' });
}
throw error;
}
// Forward to provider
await createApimartTask(reservation.id, prompt);
res.status(202).json({
reservationId: reservation.id,
status: 'pending',
estimatedSeconds: 30
});
}
Source: [api/generate-image.js](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-image.js)
Key Design Decisions
Why Reservations Instead of Immediate Deduction
The reservation pattern solves three problems:
- Provider reliability – Failed generations don't consume paid credits
- Idempotency – Duplicate callbacks from APImart are handled gracefully via reservation status checks
- Concurrency safety – PostgreSQL row-level locks prevent race conditions when multiple generations start simultaneously
Why Split Balance and Transaction Tables
profiles.credit_balanceenables O(1) availability checkscredit_transactionsprovides immutable history for disputes and analytics- The split allows caching balance reads while keeping writes transactional
Extensibility for Future Providers
Adding a new image provider requires only:
- New
providerenum value ingeneration_reservations - Provider-specific task submission logic in the API endpoint
- Payload parser in the callback handler
The core credit reservation and settlement functions remain unchanged.
Summary
- Credits are reserved, not immediately spent – The
reserve_generation_usagefunction creates a pending reservation without deducting balance - Settlement is atomic – Successful completions trigger
complete_generation_reservationto deduct credit; failures callrelease_generation_reservationwith no charge - All movements are logged – The
credit_transactionstable captures every purchase, grant, generation, and adjustment with full metadata - Admin operations use the same primitives –
grant_user_creditspowers purchases, subscriptions, refunds, and manual adjustments - Architecture is provider-agnostic – New image generation backends integrate without touching credit logic
Frequently Asked Questions
What happens if a generation fails after I start it?
Your credit is not deducted. The release_generation_reservation function marks the reservation as failed without modifying your credit_balance. You can retry immediately or contact support if failures persist.
How do free generations interact with paid credits?
The reserve_generation_usage function automatically uses your free generation slot first (tracked in profiles.free_generations_used). Only when no free slots remain will it check credit_balance >= 1 and deduct a paid credit.
Can admins manipulate credit balances arbitrarily?
Yes, through the grant_user_credits RPC and the api/admin/credits/adjust.js endpoint. However, all adjustments create permanent audit records in credit_transactions with type='admin_adjustment' and the admin's identity attached.
What prevents duplicate credit deductions from provider callbacks?
The generation_reservations.status column acts as a state machine. Both complete_generation_reservation and release_generation_reservation verify status='pending' before proceeding, making subsequent idempotent calls harmless.
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 →