Generating dozens of ad variants from one brief without losing control of spend
Updated 2026-10-17
Paid social rewards volume: the same offer in several hooks, framings and lengths, tested against each other until a winner emerges. Video generation makes variants cheap to produce, which is exactly why it is easy to overspend on them. This page covers how to structure a variant run so that it stays reproducible, cheap while you explore, and bounded by caps you set in advance.
Treat a variant as a row, not a prompt
Before any API call, define the variant space as data. A usable row has: a hook (the first second's idea), a scene description, a camera move, a mood, an aspect ratio and a duration. Twenty rows built from three hooks x two scenes x three camera moves is a controlled experiment; twenty hand-typed prompts is a pile you cannot learn from. Store each row with a stable id, because that id is what you will join against ad performance later.
Prompt templating
Keep the fixed parts of the prompt in a template and substitute only the variable parts, so differences in results come from the variables you chose:
TEMPLATE = (
"{scene}. {camera}. {mood} lighting. "
"Fast first second: {hook}. No on-screen text, no logos."
)
variants = [
dict(id=f"h{h}-s{s}-c{c}",
prompt=TEMPLATE.format(hook=HOOKS[h], scene=SCENES[s], camera=CAMERAS[c], mood="warm"))
for h in range(3) for s in range(2) for c in range(3)
]
Ask for no on-screen text. Generated lettering is unreliable; add captions and the offer in your editor or ad platform after generation, where you control spelling. The same goes for brand logos unless you are animating a supplied image.
Aspect-ratio sets
One concept usually needs three framings: 9:16 for stories and short-form feeds, 1:1 for feed placements and 16:9 for landscape and in-stream. Request each with aspect_ratio:
{"model": "kling-v3.0-std", "prompt": "...", "duration_secs": 5, "aspect_ratio": "9:16"}
Two behaviours to plan around. The API reference says an unsupported resolution and aspect-ratio combination is ignored and the model default is used, rather than rejected. And aspect_ratio values are drawn from a fixed list (16:9, 9:16, 1:1, 4:3, 3:4, 21:9), with each model supporting only some. So never assume the ratio you asked for is the ratio you got: run ffprobe on the output and, if it differs, either re-render on a model that honours the ratio or crop in post. A cheaper path for the less important placements is to generate 9:16 once and derive 1:1 by cropping, then spend generation budget only where framing genuinely changes the composition.
Draft first, then final
A variant run has two phases with different economics. In the exploration phase you want many clips, fast and cheap: a lighter model id, a short duration and a lower resolution tier. In the final phase you re-render only the handful you will actually run, on the model and tier that match your quality bar. Switching phase is a change of the model string; there is no separate "draft mode".
Remember that a re-render is a different job with a different seed, so it will not be pixel-identical to the draft. Use the draft to judge concept, composition and motion intent, and judge texture and faces only on finals. Billing is once per job at creation by requested duration, so every re-render is a new charge.
| Model | Cheapest host | Priciest host | Cheapest is | Hosts |
|---|---|---|---|---|
| bytedance/seedance-2.5 (720p) | SoCheap-Overseas $0.1419 / second | Fal-US $0.5676 / second | 75% lower | 11 |
| bytedance/seedance-2.0 (720p) | MachGen $0.1275 / second | Fal $0.3034 / second | 58% lower | 8 |
| alibaba/wan-3.0 (480p) | Replicate $0.025 / second | Alibaba $0.05 / second | 50% lower | 11 |
| google/gemini-omni-flash | Google $0.1 / second | Fal $0.13 / second | 23% lower | 4 |
| minimax/h3 (768p) | kie.ai $0.04 / second | WaveSpeedAI-resell $0.1 / second | 60% lower | 13 |
| alibaba/happyhorse-1.1 (720p) | Pika $0.098 / second | Alibaba $0.14 / second | 30% lower | 4 |
| bytedance/seedance-2.0-fast (480p) | Atlas Cloud $0.027 / second | Fal $0.2419 / second | 89% lower | 8 |
| bytedance/seedance-2.0-mini (480p) | Atlas Cloud $0.0112 / second | Fal $0.0721 / second | 84% lower | 7 |
| kling-o3 (720p) | SandBase $0.0588 / second | Tencent TokenHub $0.084 / second | 30% lower | 4 |
| kling-v3 (720p) | SandBase $0.0588 / second | Tencent TokenHub $0.084 / second | 30% lower | 8 |
| google/gemini-omni-1.1-flash (360p) | WaveSpeedAI $0.03 / second | Google $0.1 / second | 70% lower | 3 |
| lightricks/ltx-2.3-pro (2160p) | MachGen $0.1 / second | SandBase $0.32 / second | 69% lower | 2 |
Per second, before VideoRouter's 2.5% platform fee. For tiered models each row compares the resolution tier with the widest host-to-host gap. Built 2026-10-17 from the live catalog.
The table shows how much the host choice alone moves cost for the same model; letting the router pick the cheapest healthy host is the default and costs nothing extra to enable.
Deduping before you pay
Creation is billed, so duplicate submissions are real money. Three layers of protection are worth building:
- Request hash. Hash
(model, prompt, inputs, duration, aspect_ratio); if a completed job already exists with that hash, reuse it. - Semantic dedupe on the prompt side. Two rows that differ only by a synonym are one experiment. Collapse them before submission.
- Queue-level idempotence. Write the job row before calling the API and store the returned id after. If a worker crashes between the two, mark the row unknown and reconcile instead of re-submitting.
Spend caps
Put the whole run behind a dedicated API key with a monthly spend cap sized to the run, and, if you can, a model allow-list that excludes premium tiers during exploration. When the cap is hit the API returns 402 with code spend_cap_exceeded; treat that as a stop condition, not a retry. Add a client-side budget as well: estimate the run as variants x duration x tier rate before you start, and abort if the estimate exceeds your budget. Concurrency should also be bounded by a worker pool so a mistake cannot launch hundreds of jobs at once. The authentication docs describe the per-key controls.
Reading results
Join generated clips to ad-platform results by variant id, and learn at the level of the variables (hook, camera, scene), not the clip. If the "fast push-in" camera wins across three hooks, that is a finding; if one clip wins, it may be noise. Feed the findings back into the template and run the next, smaller round.
What not to automate
- Claims, prices and legal text: add them in post.
- Likenesses of real people: do not generate or animate them without rights.
- Final approval: someone should watch each clip that will carry your spend, with sound on.
For the single-product version of this workflow see the e-commerce pipeline, and for choosing models by workload see how to evaluate video APIs. The quickstart has the first call, and you can sign up and cap a key before the first batch.
Frequently asked questions
Which aspect ratios can I request for social ads?
The API reference lists 16:9, 9:16, 1:1, 4:3, 3:4 and 21:9, but each model supports only some. An unsupported combination is ignored and the model default is used, so always check the output dimensions.
How do I avoid paying twice for the same ad variant?
Hash the model, prompt, inputs, duration and aspect ratio, reuse completed jobs with the same hash, and write your own job row before calling the API so a crash does not trigger a blind re-submit.
How do I cap spending on a variant run?
Use a dedicated API key with a monthly spend cap and a model allow-list, bound concurrency with a worker pool, and treat a 402 with code spend_cap_exceeded as a stop signal.
Keep reading
- What Is a Video Generation API? How It Works and What It Costs
- Video Generation API Use Cases and the Workflow Each Needs
- Text-to-Video vs Image-to-Video vs Reference-to-Video (API)
- Build vs Buy Video Generation: Hosted API or Self-Hosting?
VideoRouter puts it next to dozens of other video and image models behind one API key, so you can compare providers, prices and fail over automatically. Compare providers on VideoRouter →