◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 26 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 26 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/AI Models
◉ AI Models/2026-06-17Advanced

Making Credits Add Up in a Rork AI Image App — Field Notes on Atomic Ledgers and Moderation

Credit billing in a Rork AI image and video app breaks in production because of the order between generation and deduction. Here are the field notes — atomic ledger consumption, idempotency, refunds on failure, and moderation — with Supabase code you can ship.

Rork611AI image generationfal.aiSupabase41credit billingidempotency3content moderationadvanced7

✦ Premium Article

The first AI image app I shipped got a support message on launch day: "Five images were generated but only one credit was deducted." It looked like a happy report, but it was proof the billing was broken. Tapping the generate button quickly five times sent five requests to the server almost simultaneously, and every one of them read the balance before any deduction landed.

The hard part of a credit-based app is not generating the image. Throw the request at fal.ai and a result comes back in a second or two. The hard part is keeping an external operation (generation) and an internal state (the balance) in agreement, every time, without drift. Get this wrong and you either give images away for free or, worse, charge for failures and collect one-star reviews. Running paid apps as a solo developer, the second one costs you more than the lost revenue — it costs trust.

These are field notes for a React Native app generated with Rork, backed by Supabase Edge Functions. I will show where the naive implementation falls apart, then move to atomic consumption, idempotency, refunds on failure, and moderation.

Three ways naive "check → generate → deduct" breaks in production

Most tutorials are written in this order: read the balance, generate if it is enough, subtract one on success. It works in development. The problem is that production has concurrency and network jitter.

Failure pathWhat happensResult
Concurrency (TOCTOU)Multiple requests read the same pre-deduction balanceSeveral images generated on a balance of 1; charges leak
Crash/timeout before deductGeneration succeeded but the deduct UPDATE never landsFree generations keep flowing
Deduct after failureThe API returned 5xx but you already subtractedCharging for failures; bad reviews

The first one is the nastiest. There is a time gap between SELECT credits and UPDATE credits = credits - 1, and another request slips into that gap. Disabling the button on the client does not save you: retries, double taps, and duplicate sends on slow networks still get through. The boundary you must defend lives inside the database, not in the client.

Hold credits as a ledger, not a balance column

Change the data model first. Stop treating a single users.credits column as the source of truth, and make a ledger — one row per change — the source of truth. The balance becomes the sum, or a derived value kept for convenience. A ledger lets you trace exactly when, on what, and how many credits were spent or returned, which makes refunds and audits simple.

-- The ledger is the source of truth for credit changes
create table credit_ledger (
  id          uuid primary key default gen_random_uuid(),
  user_id     uuid not null references auth.users(id),
  delta       integer not null,        -- purchase +, consumption -, refund +
  reason      text not null,           -- 'purchase' | 'image' | 'video' | 'refund'
  ref_id      text,                    -- generation job id or checkout session id
  idem_key    text,                    -- idempotency key (below)
  created_at  timestamptz not null default now()
);
 
-- Physically forbid double-recording under the same idempotency key
create unique index credit_ledger_idem_uniq
  on credit_ledger (user_id, idem_key)
  where idem_key is not null;
 
create index credit_ledger_user_idx on credit_ledger (user_id);

The current balance is select coalesce(sum(delta), 0) from credit_ledger where user_id = $1. If the growing ledger worries you, you can fold it into monthly snapshot rows, but at a solo-developer scale (tens of thousands of rows) the sum query is plenty fast.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦The exact paths where check-then-generate-then-deduct breaks, and how to consume atomically in a Postgres function
✦Idempotency keys that stop double charges, plus a refund ledger that reliably returns credits on failure or cancel
✦Two-stage moderation on prompt input and image output, and the operational limits that keep you through App Store review
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◉ AI Models2026-10-27
Sending temperature to the Claude API Now Returns a 400 on the New Version. Finding Every Spot in Your App
Claude API version 2026-10-21 returns a 400 if you send temperature, top_p or top_k. When it hits you, a script to scan your app and SDK setup, and the order to fix things in.
◉ AI Models2026-10-25
Is Your Rork App Still Calling Sonnet 4.5? Finding Out Before November 30
Claude Sonnet 4.5 is scheduled to retire from the API on November 30, 2026. A small script to find leftover model IDs in your app, and the order to fix them in depending on where they live.
◉ AI Models2026-10-20
Before You Type "Connect Supabase": The Two Values to Copy, and the One You Must Not
Rork asks for just two things before you type Connect Supabase in chat: a Publishable key and your project link. Here is where to copy them from, how to avoid pasting the secret key that sits on the same screen, and a small script that checks the values locally first.