All write-ups

Ürünereklam

Built

Paste 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
SCRAPER: PYTHON, HARDENEDFETCHSTRATEGY + FRAMES: GEMINIVIDEO: VEO / SEEDANCEVOICE + MUSIC: ELEVENLABSEDIT: REMOTION, CAPTIONS, CTASCORE: TRIBE V2 ON MODAL GPUSUPABASE: STATE, CREDITLEDGERFACTSFRAMESWORD TIMINGADSRANKEDONE PRODUCT LINK INSPAWN + POLL: GPU JOB OUTLIVES EDGE TIMEOUTSTEP DIES → CREDITS REFUNDED ONCE

Scroll sideways to see the whole diagram.

ArchitectureSCRAPE → GENERATE → RENDER → SCORE, REFUND ON FAILURE

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

server/inngest/functions.tsA failure handler that refunds credits exactly once
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
});
  1. 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.

  2. Line 27. One idempotency key per event: the failure handler may run again, but the ledger records the refund only once.

  3. 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.

Next write-upPistochia