Ürünereklam
BuiltPaste a product link and get finished video and image ads in Turkish and English, each one scored by a prediction model before any money is spent. It started at the BTK Akademi Hackathon in 2026.
- My role
- Design and development, on my own
- Year
- 2026
- Built with
- Next.js · Inngest · Supabase · Gemini · Veo · ElevenLabs · Remotion · PyTorch on Modal · Polar
Scroll sideways to see the whole diagram.
The problem
Ads in two languages, at the volume online shops need, usually mean an agency on a retainer. Even then nobody knows which version will work before the budget is spent.
How it is built
A Next.js app orchestrates durable Inngest jobs. A Python scraper reads the product page, with a hardened fetch path for difficult storefronts. Gemini writes the strategy, copy and frames, Veo or Seedance renders the video, ElevenLabs adds the voice, and Remotion adds word-timed captions, the watermark and the call to action. A separate studio produces static image ads and ad copy. Every variant then goes through a self-hosted model that predicts how people will react to it (TRIBE v2) on Modal GPUs, started and then polled, because a render outlives an edge function's timeout. Supabase holds the state behind row-level security with short-lived signed media URLs. Credits live in an append-only ledger with atomic charges and refunds, and checkout runs on Polar with prices in lira and dollars.
Decisions that mattered
A failed step refunds itself
The pipeline is made of durable jobs with a failure handler for each step. A step that dies refunds its credits exactly once and releases the run instead of leaving it stuck.
Media is private by default
Raw renders live in a private bucket, column-level SELECT is revoked and media links are short-lived signed URLs. No media address can be guessed, free tier included.
Every ad is scored before anyone pays for it
No ad leaves the studio without a score. The model ranks the variants before anyone puts a budget behind one.
Code from the repo
async function safeRefund(
opts: Parameters<typeof refundCredits>[0],
): Promise<void> {
try {
await refundCredits(opts);
} catch (err) {
console.error(
`[billing][RECONCILE-NEEDED] refund failed user=${opts.userId} ` +1
`amount=${opts.amount} ref=${opts.refType}:${opts.refId} ` +
`key=${opts.idempotencyKey ?? "-"}`,
err,
);
}
}
// …
await safeRefund({
supa,
userId: campaign.user_id,
amount: campaignCreditCost({
videoCount: brief.videoCount ?? 1,
videoModel: brief.videoModel ?? "seedance",
durationSec: brief.durationSec,
}),
refType: "campaign",
refId: campaignId,
idempotencyKey: `refund:campaign-all:${campaignId}`,2
description: "Kampanya başlatılamadı — otomatik kredi iadesi",3
});
Line 8. A failed refund logs a RECONCILE-NEEDED marker instead of throwing, so no credit is lost silently and the pipeline still releases the run.
Line 27. One idempotency key per event: the failure handler may run again, but the ledger records the refund only once.
Line 28. The refund reason is in Turkish because the product is bilingual from end to end.
Where it is now
It grew from a hackathon demo into a bilingual product with GPU scoring. It is closed source now, and the paid launch is in preparation.