# How Credit Transaction Deduplication Works in hasTransactionWithMetadata

> Learn how credit transaction deduplication prevents duplicate grants in hasTransactionWithMetadata by checking existing records for user ID, source, and metadata.

- Repository: [苍何/awesome-gpt-image-2](https://github.com/freestylefly/awesome-gpt-image-2)
- Tags: deep-dive
- Published: 2026-08-26

---

**Credit transaction deduplication works by querying the `credit_transactions` table for existing records matching a specific user ID, source identifier, and metadata payload, returning `true` if any match exists to prevent duplicate credit grants.**

The `awesome-gpt-image-2` repository implements a robust idempotency guard within its billing system to ensure users never receive the same credits twice. This mechanism centers on the `hasTransactionWithMetadata` helper function defined in [`api/_lib/billing.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/billing.js), which validates transaction uniqueness before inserting new credit records. Understanding this credit transaction deduplication strategy is essential when processing payment webhooks or retrying failed operations.

## The Deduplication Logic in hasTransactionWithMetadata

The core implementation resides in [`api/_lib/billing.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/billing.js) and leverages Supabase's query builder to detect potential duplicates:

```javascript
async function hasTransactionWithMetadata(client, userId, source, metadata) {
  const { data, error } = await client
    .from('credit_transactions')
    .select('id')
    .eq('user_id', userId)
    .eq('source', source)
    .contains('metadata', metadata)
    .limit(1);

  if (error) throw error;
  return Boolean(data?.length);
}

```

### Breaking Down the Query Components

- **`.eq('user_id', userId)`** – Scopes the search to the specific user receiving credits, ensuring cross-user isolation.
- **`.eq('source', source)`** – Filters by the transaction source (e.g., `"stripe_checkout"` or `"membership_grant"`), distinguishing between different credit origins.
- **`.contains('metadata', metadata)`** – Uses Supabase's JSON containment operator (`@>`) to verify that the stored metadata object includes all key-value pairs from the input. This approach ensures the check remains valid even if the stored record contains additional fields added after the original transaction.
- **`.limit(1)`** – Optimizes performance by instructing the database to stop scanning after finding the first match, reducing query cost.

The function returns `true` when `data?.length` exists, indicating a matching transaction was found.

## Preventing Duplicate Credits in grantMembershipCredits

The deduplication check serves as a critical guard clause within `grantMembershipCredits`, located in the same [`api/_lib/billing.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/billing.js) file:

```javascript
if (await hasTransactionWithMetadata(client, userId, source, metadata)) {
  // A transaction with the same metadata already exists → skip the grant
  return null;
}

```

When the function detects an existing transaction with identical metadata, it aborts the credit grant and returns `null`. This prevents duplicate credits from webhook retries, race conditions, or manual operation reruns.

### Why Idempotency Matters for Payment Processing

Payment providers like Stripe may resend webhook events during network timeouts or processing delays. Without credit transaction deduplication, these retries would create multiple credit records for a single purchase. The metadata-based check ensures that processing the same Stripe session ID twice results in only one credit grant, maintaining accurate user balances.

## Practical Code Examples

### Checking for Existing Transactions Manually

When building custom admin tools or migration scripts, you can leverage the same deduplication logic:

```javascript
import { createClient } from '@supabase/supabase-js';
import { hasTransactionWithMetadata } from './api/_lib/billing.js';

const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY);

async function verifyNoDuplicate(userId, sessionId) {
  const exists = await hasTransactionWithMetadata(
    supabase,
    userId,
    'stripe_checkout',
    { stripeSessionId: sessionId, productId: 42 }
  );
  
  if (exists) {
    console.log('Transaction already processed');
    return true;
  }
  
  // Safe to proceed with credit grant
  return false;
}

```

### Integrating the Guard in Custom Credit Flows

When extending the billing system, implement the same pattern used by `grantMembershipCredits`:

```javascript
import { hasTransactionWithMetadata } from './api/_lib/billing.js';

async function grantBonusCredits(client, userId, bonusMetadata) {
  const source = 'manual_bonus';
  
  if (await hasTransactionWithMetadata(client, userId, source, bonusMetadata)) {
    console.log('Bonus already granted');
    return null;
  }
  
  // Insert new credit transaction only if no duplicate exists
  return await client.from('credit_transactions').insert({
    user_id: userId,
    source: source,
    metadata: bonusMetadata,
    credits: 100,
    created_at: new Date().toISOString()
  });
}

```

## Database Performance Considerations

The credit transaction deduplication query is designed for scalability. The function queries indexed columns—specifically `user_id` and `source`—which should maintain database indexes in the `credit_transactions` table schema. The `.limit(1)` clause ensures that even as the table grows to millions of records, the database engine stops scanning after locating the first match, minimizing CPU and I/O overhead.

## Summary

- **Credit transaction deduplication** in `hasTransactionWithMetadata` checks for existing records matching user ID, source, and metadata to prevent double-grants.
- The function lives in [`api/_lib/billing.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/billing.js) and returns `true` when a duplicate exists, causing calling functions like `grantMembershipCredits` to abort.
- **`.contains('metadata', metadata)`** uses JSON containment to match records flexibly, accommodating schema evolution while maintaining strict deduplication.
- The query utilizes **`.limit(1)`** and targets indexed columns to ensure minimal performance impact at scale.
- This pattern ensures **idempotency** when processing payment webhooks, preventing credit inflation from retry mechanisms.

## Frequently Asked Questions

### How does hasTransactionWithMetadata detect duplicate transactions?

The function queries the `credit_transactions` table using equality filters on `user_id` and `source`, combined with a JSON containment check on the `metadata` column via `.contains()`. If any record matches all three criteria simultaneously, the function returns `true`, indicating the transaction already exists and should not be reprocessed.

### Why use .contains() instead of exact equality for metadata comparison?

Supabase's `.contains()` operator verifies that the stored JSON object includes all key-value pairs from the supplied metadata argument. This approach accommodates future schema changes where the stored record might include additional tracking fields, while still correctly identifying duplicates based on the essential identifying information (such as Stripe session IDs).

### Where is the deduplication logic implemented in the repository?

The deduplication logic is implemented in [`api/_lib/billing.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/_lib/billing.js) within the `hasTransactionWithMetadata` function (lines 81-89). It is primarily utilized by `grantMembershipCredits` (lines 56-78) in the same file to prevent duplicate membership credit grants before inserting new records into the database.

### Does the deduplication check impact database performance?

No, the check is optimized for high-volume scenarios. The query uses `.limit(1)` to stop scanning after finding the first match, and it filters on `user_id` and `source`, which should be indexed columns in the `credit_transactions` table. This design ensures consistent query performance regardless of table growth.