Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
YouTube Security VideosVisual Studio Code: VS Code Learn: Extending Agents(24.09.2026 um 21:00 Uhr)
•
YouTube Security VideosGoogle Cloud Tech: Turn Audio into Action with Gemini 3.5 Transcribe(24.09.2026 um 21:00 Uhr)
••••
Unix & Linux ServerUSN-8815-1: libass vulnerabilities(24.09.2026 um 16:57 Uhr)
•••••
YouTube Security VideosVisual Studio Code: VS Code Learn: Extending Agents(24.09.2026 um 21:00 Uhr)
•
YouTube Security VideosGoogle Cloud Tech: Turn Audio into Action with Gemini 3.5 Transcribe(24.09.2026 um 21:00 Uhr)
••••
Unix & Linux ServerUSN-8815-1: libass vulnerabilities(24.09.2026 um 16:57 Uhr)
•••••
Intelligence View
⚡ tsecurity.de Intelligence

How to build a credit system for a Next.js AI app (Stripe + Supabase)

If you're building an AI app (image generation, transcription, an agent, anything that calls a model) you've probably realized a flat "$10/month" doesn't work. Every action costs you real money in GPU/API spend, so a single power user can…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

If you're building an AI app (image generation, transcription, an agent, anything that calls a model) you've probably realized a flat "$10/month" doesn't work. Every action costs you real money in GPU/API spend, so a single power user can torch your margins. The answer is usage credits: users buy a balance, each action spends some.



Credits sound trivial. They are not. I've shipped about 10 small AI/SaaS apps, and the credit layer is where I got burned every single time. It took three patterns to fix it for good. Here they are, with copy-pasteable code for Next.js + Supabase + Stripe. Get these right and your billing won't oversell, double-charge, or strand a user's money.






The three things everyone gets wrong





  1. Overdrawing. Two requests arrive at once, both read "balance = 1," both spend. Now the balance is negative and you gave away work for free.


  2. Double-granting. Stripe retries webhooks (it will), and if you grant credits on every delivery, a $9 purchase becomes $18 of credits.


  3. Forgetting the refund. The AI job fails after you've already charged the credits. The user paid for nothing and emails you angry.



Let's kill all three.






Part 1. The atomic spend (overdraw becomes impossible)



The mistake is doing the check in your app code:




// DON'T: read-then-write has a race condition
const { balance } = await getBalance(userId);
if (balance < cost) throw new Error("insufficient");
await setBalance(userId, balance - cost); // two concurrent requests both pass the check






Do it in the database, in one statement, with the guard in the WHERE clause:




-- balances: one row per user
create table credit_balances (
user_id uuid primary key references auth.users(id) on delete cascade,
balance integer not null default 0 check (balance >= 0),
updated_at timestamptz not null default now()
);

-- append-only ledger = audit log + idempotency guard (see Part 2)
create table credit_ledger (
id bigint generated always as identity primary key,
user_id uuid not null references auth.users(id),
delta integer not null,
reason text not null,
idempotency_key text unique, -- the secret weapon
created_at timestamptz not null default now()
);

create or replace function spend_credits(p_user uuid, p_amount int, p_key text)
returns integer language plpgsql security definer set search_path = public as $$
declare new_balance int;
begin
update credit_balances
set balance = balance - p_amount, updated_at = now()
where user_id = p_user and balance >= p_amount -- the guard
returning balance into new_balance;
if new_balance is null then raise exception 'insufficient_credits'; end if;
insert into credit_ledger (user_id, delta, reason, idempotency_key)
values (p_user, -p_amount, 'spend', p_key);
return new_balance;
end $$;






where ... and balance >= p_amount is the whole trick. Under concurrent requests Postgres takes a row lock, so the second update waits for the first to commit and re-evaluates the condition against the new balance. The balance can never go negative. No application-level locking, no Redis, no race.






Part 2. Idempotent grants (Stripe retries can't double-credit)



Stripe delivers webhooks at-least-once. Your handler must be safe to run twice with the same event. Key every grant on the Stripe event id and let the ledger's unique constraint enforce it:




// app/api/stripe/webhook/route.ts
export async function POST(req: Request) {
const body = await req.text(); // raw body, required for signature check
const event = stripe.webhooks.constructEvent(
body, req.headers.get("stripe-signature")!, process.env.STRIPE_WEBHOOK_SECRET!
);

if (event.type === "checkout.session.completed") {
const s = event.data.object as Stripe.Checkout.Session;
const userId = s.metadata?.userId;
const credits = creditsForSession(s); // map your Price to credits
// idempotencyKey = event.id, so a retry is a no-op
await grantCredits({ userId, amount: credits, idempotencyKey: event.id });
}
return Response.json({ received: true });
}









create or replace function grant_credits(p_user uuid, p_amount int, p_key text)
returns integer language plpgsql security definer set search_path = public as $$
declare new_balance int;
begin
-- already applied this event? return current balance unchanged.
if exists (select 1 from credit_ledger where idempotency_key = p_key) then
select balance into new_balance from credit_balances where user_id = p_user;
return coalesce(new_balance, 0);
end if;
insert into credit_balances (user_id, balance) values (p_user, p_amount)
on conflict (user_id) do update set balance = credit_balances.balance + p_amount
returning balance into new_balance;
insert into credit_ledger (user_id, delta, reason, idempotency_key)
values (p_user, p_amount, 'purchase', p_key);
return new_balance;
end $$;






The exists check is a fast path. The real guarantee is idempotency_key unique. If two retries race past the check, the second insert into the ledger violates the constraint and the whole function (it's atomic) rolls back, including the balance bump. Applied exactly once.



A bonus: now you can safely return 500 on any transient error and let Stripe retry, because retries are free.






Part 3. The worker and refund-on-fail



Don't run a slow model call inside the request. It'll time out. Spend the credits, queue a job, process it in a worker, and refund if it fails:




// app/api/jobs/route.ts: spend first, then enqueue
const jobId = crypto.randomUUID();
try {
await spendCredits({ userId, amount: 5, idempotencyKey: jobId }); // keyed on jobId
} catch (e) {
return Response.json({ error: "insufficient_credits" }, { status: 402 });
}
await db.from("jobs").insert({ id: jobId, user_id: userId, type, status: "queued", credits_cost: 5 });









// worker.ts: claim, process, refund on failure
const { data: job } = await db.rpc("claim_next_job"); // FOR UPDATE SKIP LOCKED
try {
const output = await runYourModel(job);
await db.from("jobs").update({ status: "done", output }).eq("id", job.id);
} catch (err) {
await db.from("jobs").update({ status: "failed", error: String(err) }).eq("id", job.id);
await refundCredits({ userId: job.user_id, amount: job.credits_cost,
idempotencyKey: `refund:${job.id}` }); // distinct key, never double-refunds
}






Two details that matter. Claim jobs with FOR UPDATE SKIP LOCKED so you can run N workers without two of them grabbing the same job. And key the refund refund:<jobId>, distinct from the spend's <jobId>, so a retried failure handler can't refund twice.






The pitfalls, in one list




  • Check the balance in app code instead of the WHERE clause and you oversell.

  • Grant on webhook delivery instead of a unique event id and you double-credit.

  • No refund path means angry users and chargebacks.

  • Run the model in the request and you get timeouts at scale.

  • Reuse one idempotency key for spend and refund and they collide. Use distinct prefixes.






That's the whole pattern



Three functions (spend, grant, refund), one ledger table doing double duty as audit log and idempotency guard, and a worker. It won't oversell, double-charge, or strand money. It took me a few shipped apps to get here.



If you'd rather not rebuild it, I packaged all of this into CreditKit, a Next.js + Supabase + Stripe starter for AI apps. That's these patterns plus passwordless auth, Stripe checkout and portal, the worker, and a dashboard. Either way, now you know the traps.



Algy (youngalgy.com). I build and ship small AI/SaaS products. CreditKit is the billing layer extracted from about 10 of them.

CTI Threat Relationship Graph2 Knoten / 1 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
SOC Incident Playbook: Remote Code Execution (RCE) Defense
title: Detect Exploitation - How to build a credit system for a Next.js AI app (Stripe + Supabase)
id: e7cbccd9-3580-40ce-837e-849034352258
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-24
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-24"
        description = "YARA Signature for "
    strings:
        $str = "How to build a credit system f" ascii wide
    condition:
        any of them
}
index=security sourcetype IN ("cisco:asa", "pan:traffic", "zeek_conn", "suricata", "WinEventLog:Security")
("How to build a credit system for a Nextj")
| stats count earliest(_time) as first_seen latest(_time) as last_seen by src_ip, dest_ip, dest_host, signature
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S"), last_seen=strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| sort - count
message: "*How to build a credit system for a Nextj*"
CommonSecurityLog
| where Message has "How to build a credit system for a Nextj"
| summarize EventCount = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SourceIP, DestinationIP, DestinationPort, Activity
| extend DetectionRule = "iShareStuff-CTI-Compiled"
| sort by EventCount desc
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich How to build a credit system for a Next..... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

⚡ Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How to build a credit system for a Next.js AI app (Stripe + Supabase)

Thematisch verwandte Begriffe: build, credit, system, Nextjs · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-57175 | Python Social Auth is a social authentication/registration mechanism. Pr…
Advisory →
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel • Rechts: nächster Artikel • unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...
↗ Original-Quelle