AI video generation APIs charge per call — typically $0.10 to $2.00 per clip depending on duration, resolution, and provider. That sounds cheap until an editor generates ten hook candidates, batch-runs five section swaps, and regenerates twice because copy was too long. A single session can burn $15–$40 on outputs that never ship.
Pre-spend guards and content-aware routing are the two patterns I shipped across Hook Machine and Variant Multiplier to stop wasting money before API calls fire. This post covers validation-before-spend, engine selection by content profile, policy self-healing, live cost preview, and transient error retry — the orchestration layer that makes multi-provider video generation economically viable.
Fail before spending is the most valuable pattern in AI API orchestration. Every dollar not spent on a clip that would have failed validation is a dollar available for a clip that might ship.
The cost problem
Video generation is not like text completion. You cannot cheaply preview the output. A failed generation still costs full price — and failures are common:
- Copy overrun — 25 words of VO for a 3-second hook slot
- Corrupt seed frame — wrong aspect ratio, truncated upload
- Content policy rejection — engine refuses the prompt category
- Transient 422 — provider soft-reject during capacity spikes
- Wrong engine selection — premium engine used for product-only motion
| Provider | Typical cost (3s clip) | Typical cost (15s clip) | Failure rate (beta) |
|---|---|---|---|
| Seedance | $0.10–$0.20 | $0.40–$0.80 | 8% |
| Kling (via Replicate) | $0.30–$0.50 | $1.00–$1.50 | 12% |
| fal Veo | $0.50–$0.80 | $1.50–$2.00 | 15% (incl. transient 422) |
| ElevenLabs TTS | $0.03–$0.08 | $0.10–$0.25 | 3% |
At 15% failure rate on a $0.80 clip, one in seven generations is pure waste. Multiply by batch runs of 10 and the waste compounds fast.
Pre-spend guards
feat/pre-spend-guards validates inputs before any paid API call. Two checks cover the most common pre-generation failures:
Copy-length preflight
Estimate spoken duration from word count and target WPM. Block generation if estimated duration exceeds the slot ceiling.
interface CopyPreflightResult {
passed: boolean;
wordCount: number;
estimatedDurationMs: number;
ceilingMs: number;
reason?: string;
}
function preflightCopyLength(
copy: string,
ceilingMs: number,
wpm: number = 160,
): CopyPreflightResult {
const words = copy.trim().split(/\s+/);
const estimatedDurationMs = (words.length / wpm) * 60 * 1000;
return {
passed: estimatedDurationMs <= ceilingMs,
wordCount: words.length,
estimatedDurationMs,
ceilingMs,
reason: estimatedDurationMs > ceilingMs
? `Copy too long: ${words.length} words ≈ ${(estimatedDurationMs / 1000).toFixed(1)}s, max ${(ceilingMs / 1000).toFixed(1)}s`
: undefined,
};
}
For Hook Machine's 3-second hooks, the ceiling is 3000ms with a 2.8s spoken target (200ms buffer for TTS pacing variance). Copy preflight blocked ~18% of generation attempts during beta — all of which would have produced unusable clips.
Clip health check
Validate seed frame and source video integrity via ffprobe before passing to the video engine — resolution, aspect ratio, minimum file size, and decodeability. Clip health catches truncated Google Drive downloads, wrong-aspect seed frames from the image dispatch chain, and corrupt uploads — all of which would fail at the video engine with a charged API call.
Content-aware routing
feat/content-aware-routing inspects the generation brief and selects the cheapest engine capable of handling the content. The router classifies briefs by content profile:
type ContentProfile =
| 'product-only'
| 'ugc-presenter'
| 'high-motion-lifestyle'
| 'policy-sensitive';
function classifyBrief(brief: GenerationBrief): ContentProfile {
if (brief.hasHumanSubject && brief.requiresLipSync) return 'ugc-presenter';
if (brief.motionLevel === 'high') return 'high-motion-lifestyle';
if (brief.category in POLICY_SENSITIVE_CATEGORIES) return 'policy-sensitive';
return 'product-only';
}
const ENGINE_PRIORITY: Record<ContentProfile, EngineId[]> = {
'product-only': ['seedance', 'kling', 'fal-veo'],
'ugc-presenter': ['kling', 'fal-veo', 'replicate-fallback'],
'high-motion-lifestyle': ['fal-veo', 'kling', 'seedance'],
'policy-sensitive': ['replicate-fallback', 'seedream', 'seedance'],
};
| Content profile | Primary engine | Cost savings vs always-Veo |
|---|---|---|
| Product-only | Seedance | ~75% |
| UGC presenter | Kling | ~40% |
| High motion | fal Veo | 0% (appropriate spend) |
| Policy-sensitive | Replicate fallback | Varies |
Content-policy self-healing
When the selected engine rejects content (HTTP 422 with policy message, or explicit content filter response), the router automatically retries with the next engine in the fallback chain:
async function generateWithSelfHeal(
brief: GenerationBrief,
engines: EngineId[],
): Promise<GenerationResult> {
const errors: EngineError[] = [];
for (const engineId of engines) {
const engine = engineRegistry.get(engineId);
try {
const result = await engine.generate(brief);
await auditLog.record('generation.success', { engineId, briefId: brief.id });
return result;
} catch (err) {
errors.push({ engineId, error: err });
if (isContentPolicyRejection(err)) {
await auditLog.record('generation.policy_reject', { engineId, briefId: brief.id });
continue; // try next engine
}
if (isTransient422(err)) {
const retried = await retryWithBackoff(() => engine.generate(brief));
if (retried) return retried;
continue;
}
throw err; // non-recoverable
}
}
throw new AllEnginesExhaustedError(brief.id, errors);
}
Self-healing saved manual editor intervention on ~22% of policy-sensitive generations during beta. Without it, editors saw opaque error messages and had to manually retry with a different engine.
Live cost preview
Variant Multiplier's studio workstreams added live cost estimation before spend:
feat/studio-ws1-backend—POST /config/estimateendpointfeat/studio-ws3-modes-cost— per-section mode selection with live cost update
interface CostEstimate {
totalUsd: number;
breakdown: {
video: number;
tts: number;
llm: number;
image: number;
};
engines: {
video: EngineId;
tts: EngineId;
image: EngineId;
};
}
function estimateCost(
section: SectionBounds,
mode: SectionMode,
profile: ContentProfile,
): CostEstimate {
const durationSec = (section.endMs - section.startMs) / 1000;
const engines = ENGINE_PRIORITY[profile];
const videoEngine = engineRegistry.get(engines[0]);
return {
totalUsd: videoEngine.pricePerSecond * durationSec + /* tts + llm */,
breakdown: { /* ... */ },
engines: { video: engines[0], tts: 'elevenlabs', image: 'nano-banana-pro' },
};
}
The UI updates cost in real time as editors change section boundaries or switch modes (rewrite-only vs full B-roll regen). No-auto-spend UX ensures the estimate is visible before the "Run" button fires any API call.
Transient error retry: fal Veo 422
fix/fal-veo-transient-422 handles fal's Veo endpoint returning HTTP 422 during capacity spikes — a soft "try again later" signal, not a content rejection. The fix applies exponential backoff (3 retries, 2–16s with jitter) on transient 422s while routing policy 422s to the next engine. The error classifier inspects response body fields (rate_limit, overloaded vs content_policy, safety), not just status code.
| Error type | HTTP signal | Action |
|---|---|---|
| Transient capacity | 422 + "rate_limit" / "overloaded" | Retry same engine with backoff |
| Content policy | 422 + "content_policy" / "safety" | Fallback to next engine |
| Invalid input | 400 / 422 + validation details | Block, surface to editor (pre-spend should catch) |
| Provider outage | 503 / 504 | Retry then fallback |
Composing the guard stack
In production, all patterns compose in fixed order: copy preflight → clip health → cost estimate → content classification → generate with self-heal. Wasted spend per 100 generations dropped from ~$12–$18 to ~$3–$5; editor-visible failures from 15% to 4%; average cost per shipped hook from $1.40 to $0.65.
What I'd do differently
I would centralize pre-spend guards in a shared library from day one, not ship them independently in Hook Machine and Variant Multiplier. The copy preflight and clip health checks are identical; we duplicated them and had to reconcile minor differences in WPM defaults and aspect ratio tolerances.
I would also log every blocked preflight attempt with the reason code. We know blocked attempts saved spend, but we do not know which guard fires most often — data that would prioritize prompt template improvements over clip health checks.
The pattern: fail before spending
AI video APIs will get cheaper over time. They will not get more predictable — content policy, capacity spikes, and input sensitivity are structural properties of generative models. Building validation, routing, and retry as an orchestration layer above the providers keeps spend proportional to shippable output, not attempted output.