How to Reserve Generation Credits Before Submitting to APIMart

The system reserves generation credits by calling the Supabase stored procedure reserve_generation_usage to create a pending reservation row before invoking APIMart, ensuring atomic credit handling and preventing balance overdrafts.

In the freestylefly/awesome-gpt-image-2 repository, the platform implements a defensive credit reservation pattern to protect user balances during third-party API calls. This workflow guarantees that credits are only consumed after successful APIMart job submission, while handling failures gracefully through rollback mechanisms.

The Three-Layer Reservation Architecture

The reservation system operates through three coordinated components: a PostgreSQL stored procedure for atomic database operations, a Node.js wrapper for error translation, and the HTTP handler for orchestration.

Supabase Stored Procedure

The core logic resides in supabase/migrations/20260509090000_membership_billing.sql, specifically within the reserve_generation_usage function (lines 280–306). This PostgreSQL stored procedure accepts four parameters:

  • p_user_id – The UUID of the requesting user
  • p_case_id – Integer identifier for the generation context
  • p_prompt – The text prompt being submitted
  • p_force_credit – Boolean flag allowing administrators to bypass free generation allowances

The procedure inserts a row into public.generation_reservations with status pending and calculates the credit_amount based on the user's remaining free generations. It returns a reservation_id UUID that tracks the transaction throughout its lifecycle.

Node.js Wrapper Function

The reserveGeneration function in api/generate-image.js (lines 30–48) serves as the RPC client wrapper. It translates database errors into HTTP semantics—specifically converting Supabase exceptions containing CREDITS_REQUIRED into a 402 Payment Required response.

// src/api/generate-image.js – reserveGeneration
export async function reserveGeneration(client, profile, userId, caseId, prompt) {
  const { data, error } = await client.rpc('reserve_generation_usage', {
    p_user_id: userId,
    p_case_id: caseId,
    p_prompt: prompt,
    p_force_credit: Boolean(profile?.isSuperAdmin)
  });

  if (error) {
    const msg = String(error.message || error.details || '').toUpperCase();
    if (msg.includes('CREDITS_REQUIRED')) {
      const e = new Error('CREDITS_REQUIRED');
      e.code = 'CREDITS_REQUIRED';
      throw e;
    }
    throw error;
  }

  const row = Array.isArray(data) ? data[0] : data;
  if (!row?.reservation_id) throw new Error('RESERVATION_FAILED');
  return { reservationId: row.reservation_id };
}

Handler Orchestration

The HTTP handler coordinates between credit reservation and APIMart submission. After obtaining the reservationId, it immediately calls submitApimartGeneration from api/_lib/apimart.js, then updates the reservation row with the third-party task identifier.

Step-by-Step Implementation

Implementing the reservation flow requires sequential database operations with strict error handling at each phase.

Creating the Reservation

First, invoke the stored procedure through the authenticated Supabase client:

reservation = await reserveGeneration(
  auth.client,
  auth.profile,
  auth.user.id,
  caseId,
  prompt
);

This creates the generation_reservations row with status pending, effectively locking the user's credit balance without finalizing the deduction.

Handling Insufficient Credits

When the stored procedure detects insufficient credits (and p_force_credit is false), it raises an exception. The wrapper catches this and throws a coded error that translates to HTTP 402:

// Results in 402 response to client
if (msg.includes('CREDITS_REQUIRED')) {
  const e = new Error('CREDITS_REQUIRED');
  e.code = 'CREDITS_REQUIRED';
  throw e;
}

Linking the APIMart Task

Upon successful reservation, submit to APIMart and persist the external task ID:

// Submit to APIMart
const submitted = await submitApimartGeneration({
  apiKey: config.apiKey,
  prompt,
  language: body.language,
  webhook: `${appUrl}/api/generation`
});

// Link the APIMart task to the reservation
await auth.client
  .from('generation_reservations')
  .update({
    provider: 'apimart',
    provider_task_id: submitted.taskId
  })
  .eq('id', reservation.reservationId)
  .eq('user_id', auth.user.id);

Error Handling and Rollback Mechanisms

If the submitApimartGeneration call throws an exception, the handler must release the reservation to prevent orphaned locks on the user's credit balance. The system invokes the release_generation_reservation RPC to mark the reservation as failed and restore the pending credit allocation.

This compensating transaction pattern ensures that users are never charged for generations that fail to reach APIMart, while the pending status in the reservation table enables audit trails for debugging failed submissions.

Summary

  • Atomic credit protection: The reserve_generation_usage procedure creates a pending row in generation_reservations before any third-party API calls, preventing race conditions and overdrafts.
  • Error translation layer: The reserveGeneration wrapper in api/generate-image.js converts database errors into appropriate HTTP status codes, specifically 402 for credit deficiencies.
  • Task correlation: After APIMart accepts the job, the system stores the provider_task_id in the reservation row, linking internal credit tracking to external task identifiers.
  • Automatic rollback: Failed APIMart submissions trigger release_generation_reservation, ensuring credits remain available for retry attempts.

Frequently Asked Questions

What happens if APIMart submission fails after reservation?

The handler catches the exception from submitApimartGeneration and calls the release_generation_reservation RPC. This marks the reservation as failed and releases the held credits back to the user's available balance, maintaining consistency between the platform's credit ledger and actual usage.

How does the system handle concurrent generation requests?

The reserve_generation_usage stored procedure executes as a SECURITY DEFINER function within PostgreSQL, providing atomic isolation. When multiple requests arrive simultaneously, each creates a separate pending reservation row. The credit calculation occurs within the same transaction as the insert, preventing overspending scenarios even under high concurrency.

Can administrators bypass credit requirements?

Yes. The p_force_credit parameter in the stored procedure, passed via profile.isSuperAdmin, allows administrators to reserve generations regardless of their credit balance. When this flag is true, the system reserves the generation but marks it as forced, enabling supervisory overrides while maintaining audit records in the force_credit column.

Where is the reservation schema defined?

The generation_reservations table schema and the reserve_generation_usage function are defined in supabase/migrations/20260509090000_membership_billing.sql. This migration establishes the credit reservation infrastructure alongside membership and billing tables, ensuring referential integrity between reservations, user profiles, and case records.

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 →