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, and credit_transactions tables
  • Reservation layer – generation_reservations with pending/completed/failed states
  • Settlement layer – RPC functions reserve_generation_usage, complete_generation_reservation, and release_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:

  1. Checks if free_generations_used < 1 → uses free slot if available
  2. Otherwise verifies credit_balance >= 1
  3. Creates a generation_reservations row with:
    • status: 'pending'
    • type: 'generation' (or 'free' for complimentary uses)
    • provider: 'apimart'
  4. Returns the reservation_id for 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:

  1. User submits prompt → POST /api/generate-image
  2. Reserve credit → reserve_generation_usage RPC executes
  3. Queue with provider → APImart task created with reservation ID
  4. Async processing → Image generates (seconds to minutes)
  5. Provider callback → POST /api/generation/callback
  6. Find reservation → findPlatformGeneration helper locates pending row
  7. Settle outcome → complete_generation_reservation or release_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_balance enables O(1) availability checks
  • credit_transactions provides 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:

  1. New provider enum value in generation_reservations
  2. Provider-specific task submission logic in the API endpoint
  3. 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_usage function creates a pending reservation without deducting balance
  • Settlement is atomic – Successful completions trigger complete_generation_reservation to deduct credit; failures call release_generation_reservation with no charge
  • All movements are logged – The credit_transactions table captures every purchase, grant, generation, and adjustment with full metadata
  • Admin operations use the same primitives – grant_user_credits powers 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →