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 userp_case_id– Integer identifier for the generation contextp_prompt– The text prompt being submittedp_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_usageprocedure creates apendingrow ingeneration_reservationsbefore any third-party API calls, preventing race conditions and overdrafts. - Error translation layer: The
reserveGenerationwrapper inapi/generate-image.jsconverts database errors into appropriate HTTP status codes, specifically402for credit deficiencies. - Task correlation: After APIMart accepts the job, the system stores the
provider_task_idin 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →