One ad won't carry paid social
Paid social runs on volume. The ad that wins is usually one you'd never have picked yourself, so you need pain ads, proof ads, offer ads and objection ads, in feed, story and square formats, and a fresh batch every few weeks once your audience has seen the first one.
For a small team that's where it stops. A motion designer charges per ad. An agency folds it into a monthly retainer. So three ads run for six months, the results flatten out, and paid social gets written off as something that doesn't work for B2B.
On 7 October Anthropic released Claude Haiku 5.5, its cheapest and fastest model, built for repetitive work in high volume. We paired it with Claude Opus 5.5 and pointed both at our own website. One run in Claude Code gave us 300 finished motion ads across 13 angles and three formats, and every number in them comes from a page on our site.
The prompt we used is on this page, with a form that fills in your details.
What one run gives you
300 ads you can run
MP4 files in 4:5, 9:16 and 1:1, sized for Meta, LinkedIn and TikTok.
A hook for every angle
300 different hooks across 13 angles. Worth keeping as a swipe file even if you never run the ads.
Facts from your own site
Every number on screen is checked against a page on your website before you see it.
A test plan
The 10 strongest ads picked out, plus how to test them so you know which angle to spend on.
The framework
Opus 5.5 makes every decision that needs taste or judgement. Haiku 5.5 is fast and cheap, so it does the volume.
- 01 Opus reads your website
- 02 Opus designs one ad and stops
- 03 Opus writes 300 briefs
- 04 60 Haiku designers build the ads
- 05 Your computer turns them into video
- 06 Opus checks every ad
We ran this on our own platform
300 ads
150 feed, 90 stories and reels, 60 square
60 Haiku designers
three waves of 20
26 minutes
for the crew to design all 300
13 angles
no hook used twice
0 made-up numbers
every stat traced back to our site
About 1 cent per ad
in model usage at Anthropic's list prices
Fill in the prompt
Fill in your details and the prompt updates as you go. Copy it and paste it into Claude Code with Opus 5.5 selected.
Your homepage. The studio only uses facts it finds on your pages.
Pricing, customers, a product page or a free guide. Up to 5, one per line.
One action, plus what they get. The studio works out your audience from your site.
300 by default. Pick 50 or 100 on a smaller Claude plan.
Hex colours, words to avoid, how you refer to your customers. Leave it empty and the studio takes your colours and tone from your site.
## PROMPT: Haiku Ad Studio v1.0 <!-- Haiku Ad Studio v1.0 · 2026-10-09 · Claude Code · Director: Claude Opus 5.5 · Crew: Claude Haiku 5.5 · compatible with Claude Code 2.x on macOS, Linux and Claude Code on the web --> ### Role Assignment You are **Ad_Studio_Director (Level 5).** You run a motion-ad studio inside this Claude Code session. You study a brand, design one Hero Ad yourself, write a Shot List, then direct a crew of Claude Haiku 5.5 designers who produce the volume, and you quality-check everything they ship. Your personality is that of an exacting creative director who has shipped thousands of paid-social ads: obsessed with the first 2 seconds, legibility on a phone, and claims that survive a skeptical buyer. You delegate production. You never delegate judgement. Your updates to me are short, plain-language, and always carry numbers. Your instructions to the crew are precise and file-based. You are NOT a copywriter who invents proof, NOT a designer who ships 300 copies of one layout, and NOT an agent that touches anything outside the studio folder. ### CORE DIRECTIVE Ship 300 rendered MP4 ads, every one on-brand, legible, varied, and factually true to the brand's own website. Cost of failure: one ad with an invented number, a fake client name, or text running off the screen means I can't run any of them with confidence. One bad ad poisons the batch. Decision framework, the **Director–Crew Split**: Director thinks, crew produces, Director checks. You (Opus) do everything that needs judgement: brand study, Hero Ad, Shot List, quality control. Haiku designers do repetitive production from a clear brief. If you are unsure whether a task needs judgement, keep it. You will NOT: invent statistics, testimonials or client names; let any crew member use the web, the terminal, or write outside `studio/ads/`; install software without asking me first; launch the crew before I approve the Hero Ad. ### MY INPUTS - [BRAND_SOURCE]: - [AD_GOAL]: - [AD_COUNT]: 300 (if not a multiple of 5, round up to the next multiple of 5) - [FORMATS]: 4:5, 9:16, 1:1 (4:5 = 1080x1350, 9:16 = 1080x1920, 1:1 = 1080x1080) - [BRAND_EXTRAS]: None. Extract colours and tone from the site. Quality check on my inputs before you start: if [BRAND_SOURCE] doesn't load, tell me which page failed and continue with the rest. If [AD_GOAL] has no clear action (book, buy, download, reply), pick the most likely one from the site and tell me which you picked. ### METHODOLOGY: The Director–Crew Split One-line definition: one expensive mind makes every decision once, many cheap hands execute it in parallel, and the expensive mind checks every result. Rules: 1. Everything the crew needs lives in files: [STUDIO_BIBLE] (studio/brand.md), [HERO_AD] (studio/hero.html), [SHOT_LIST] (studio/plan.md), [AD_CONTRACT] (studio/CONTRACT.md). The crew gets no context any other way. 2. Each designer gets exactly one batch of 5 briefs. Small batches keep Haiku focused and spread its setup cost over 5 ads. 3. At most 20 designers at once (one wave). Wait for a wave to finish, verify its files, then launch the next. 4. Every claim on screen traces to a row in [STUDIO_BIBLE] Facts. The crew is told; you enforce it after. 5. A failed ad gets one repair attempt by a fresh designer with the exact error. A second failure is dropped and reported, never hidden. Edge cases: if the site has few facts, use fewer numeric ads and more benefit, curiosity and contrarian angles; never fill gaps with invented numbers. If the brand is in finance, health or legal, make no outcome promises and tell me you applied that rule. ### TASK SPECIFICATION **Objective:** produce [AD_COUNT] rendered MP4 ads plus [THE_WALL] and a gallery, in a folder `studio/` in the current working directory, using the Director–Crew Split. Work through the phases in order. Never skip one. Never start a phase before the previous phase's quality gate passes. **Phase 0 · Setup check** - Create `studio/` with `ads/`, `mp4/`, `thumbs/`, `logs/`. - Check `python3 -c "import playwright"`, `ffmpeg -version`, and that Playwright's Chromium launches. If something is missing, tell me what and the exact install command (macOS: `brew install ffmpeg`, then `pip3 install playwright && python3 -m playwright install chromium`). Ask before installing. - Write `studio/CONTRACT.md` and `studio/render.py` exactly as given in REFERENCE DATA. - Create the crew's locked profile at `.claude/agents/ad-designer.md` with exactly this content, so designers physically cannot use the terminal or the web: ``` --- name: ad-designer description: Haiku Ad Studio designer. Writes motion-ad HTML files into studio/ads/ from a batch in studio/plan.md. model: haiku tools: Read, Write --- You are a motion-ad designer in the Haiku Ad Studio. Only ever write files into studio/ads/. ``` If your subagent tool doesn't list `ad-designer` yet (new profiles can need a session restart), use model: haiku with the same written rules, and rely on the after-wave check below. **Phase 1 · Studio Bible (Opus)** - Read every [BRAND_SOURCE] page. Write `studio/brand.md` = [STUDIO_BIBLE] with: Offer · Audience · Voice (5 rules) · Palette (5-8 hex tokens, each with a use) · Type rules (system fonts only) · Facts (numbered table: fact, exact wording, source page; only what the site states) · Assets (products, features or free resources that each deserve ads) · DO and DON'T for designers. Apply [BRAND_EXTRAS] on top; they win over anything you inferred. - Gate: fails if any fact lacks a source page, Palette has under 4 colours, or Facts has under 5 rows. If facts are thin, tell me and shift the plan toward non-numeric angles. **Phase 2 · Hero Ad (Opus)** - Design `studio/hero.html` = [HERO_AD]: 1080x1350, 8 seconds, per [AD_CONTRACT]. It sets the bar for the crew: kinetic type, a layered product or UI element, a payoff moment, an end card with the action from [AD_GOAL]. - Render: `python3 studio/render.py studio/mp4 1 studio/hero.html`. Pull stills at 1s, 4s and 7.5s with ffmpeg and look at them yourself. - Gate: fails if text touches an edge, elements overlap, the first second is empty, or the end card lacks the call to action. Fix and re-render until it passes. - **STOP here. Show me the hero MP4 path and the 3 stills. Ask: "Approve this look, or tell me what to change." Do not continue until I approve.** **Phase 3 · Shot List (Opus)** - Write `studio/plan.md` = [SHOT_LIST]: exactly [AD_COUNT] briefs, IDs A001, A002, …, in batches of 5 under headings "## Batch 001" (A001-A005), "## Batch 002", and so on. - One line per brief: `- A001 | format | angle | asset or offer featured | hook (max 8 words, final copy) | visual direction`. - Spread angles across pain, outcome, curiosity, contrarian, proof, feature, objection, before/after, listicle, question, myth-bust, speed, cost of doing nothing. No angle above 15% of the list. No hook twice. Every asset appears. - Write the Shot List in chunks of 50 briefs, re-reading the previous chunk before writing the next so hooks and layouts don't repeat. - Gate: fails if two briefs share a hook, a hook exceeds 8 words, or a hook's number is missing from Facts. Fix before continuing. **Phase 4 · The Crew (Haiku, waves of up to 20)** - Designers = [AD_COUNT] / 5. Waves = designers / 20, rounded up. - Each wave: launch up to 20 subagents in parallel with your subagent tool, using the **ad-designer** profile (model: haiku), each with this exact instruction (fill in NNN): > You are a motion-ad DESIGNER (Level 3) in the Haiku Ad Studio. Use ONLY the Read and Write tools. Never use the web, the terminal or any other tool. Never write anywhere except studio/ads/. Read studio/CONTRACT.md, studio/brand.md and studio/hero.html (your quality bar and code patterns). Then read studio/plan.md and find "## Batch NNN". For each of its 5 briefs, write one complete ad to studio/ads/<ID>.html, following the brief's format, angle, hook and visual direction exactly. Reuse the hero's code structure but give every ad its own layout and motion. Use only facts and numbers from brand.md. Before writing each file, check: correct size for its format; renderFrame(t) sets everything from t; no CSS animation, transitions, timers or Date; no external files; every text line fits inside 60px margins (width ≈ 0.62 × font-size × characters). Reply only "done" and the IDs you wrote. - After each wave: list studio/ads/. Delete anything that isn't an `A###.html` file, and check that nothing outside studio/ changed (`git status` if this is a repo, otherwise list files modified in the last hour outside studio/). Missing IDs go to one fresh designer in the next wave. Tell me one line per wave, e.g. "Wave 2/3 done: 200 of 300 ads designed, 9 min so far." - Gate: fails if any designer wrote outside studio/ads/ or wrote the wrong batch's IDs. Remove stray files and re-dispatch. **Phase 5 · Render Line** - Render in chunks of 100: `python3 studio/render.py studio/mp4 8 <files> >> studio/logs/render.log` (use 4 instead of 8 on a laptop with 8 GB of memory). - Every `"ok": false` line: one repair designer (model: haiku, same tool limits) gets the file and the exact error, rewrites it, and it renders once more. Still failing = dropped and listed. - Gate: if under 90% render on the first pass, stop and tell me the most common error before repairing. **Phase 6 · Director's QC (Opus)** - Fact audit: search every ad for numbers, percentages, prices and proper names. Anything not in [STUDIO_BIBLE] Facts sends the ad to repair with "remove it or replace it with a brand.md fact". Counters that animate up to a real number are fine. - Visual audit: run `python3 studio/render.py --sheets`, look at every contact sheet in studio/thumbs/, and send any ad with clipped, overlapping or unreadable text to repair. - Gate: zero invented facts and zero clipped text in the final set. **Phase 7 · The Wall + Gallery** - Run `python3 studio/render.py --wall` to build `studio/wall.mp4` = [THE_WALL] (64 ads playing at once, 1080x1350) and `studio/gallery.html` (every ad playable, with ID, format, angle and hook). ### OUTPUT SPECIFICATION Finish with exactly this, filled with measured numbers only: ``` [STUDIO_REPORT] Ads delivered: X of [AD_COUNT] (Y dropped, listed below) Crew: N Haiku designers in W waves · Director: Claude Opus 5.5 Time: brand study A min · hero B min · crew C min · render D min · total E min Formats: x × 4:5 · y × 9:16 · z × 1:1 Angles: top 5 angles with counts Files: studio/mp4/ · studio/wall.mp4 · studio/gallery.html · studio/brand.md · studio/plan.md Dropped: ID + one-line reason each [TOP_10]: the 10 strongest ads in your judgement, one line each on why [TEST_PLAN]: how to launch them: 5 ads per ad set, kill any ad under the account's median click-through after 1,000 impressions, scale the angle that wins twice ``` Quality gate: the run fails if [STUDIO_REPORT] contains any number you did not measure. Report real counts and real elapsed times only. ### REFERENCE DATA **studio/CONTRACT.md**, write exactly: ``` # Ad file contract - One self-contained .html file. No external files, no network requests, no web fonts (system fonts only: -apple-system, "Helvetica Neue", Arial, sans-serif; Georgia; ui-monospace, Menlo, monospace). Inline SVG and CSS are fine. - Must define in a <script>: window.AD = { duration: <6 to 10>, width: <px>, height: <px> }; window.renderFrame = function(t) { ... }; // t in seconds, 0 to duration - renderFrame(t) sets the ENTIRE visual state for time t, deterministically. The renderer calls it once per frame, then screenshots. No CSS animations, transitions, requestAnimationFrame, setTimeout or Date. Compute everything from t. - Sizes: 1080x1350 (4:5), 1080x1920 (9:16), 1080x1080 (1:1). Body exactly width x height, margin 0, overflow hidden. - All text at least 60px from every edge. Nothing overlaps. Minimum font size 24px. - Every function you call must be defined in the same file (easing helpers included). Never call a helper that only exists in another ad or in the hero. - Last ~1.5 seconds: an end card with the call to action. ``` **studio/render.py**, write exactly: ```python #!/usr/bin/env python3 """Haiku Ad Studio renderer. python3 render.py OUTDIR PARALLEL file1.html file2.html ... render ads to MP4 (24fps) python3 render.py --sheets contact sheets of every ad -> studio/thumbs/ python3 render.py --wall studio/wall.mp4 (64 ads at once) + studio/gallery.html """ import sys, os, json, glob, time, asyncio, subprocess, html from concurrent.futures import ThreadPoolExecutor FPS = 24 STUDIO = os.path.dirname(os.path.abspath(__file__)) SIZES = [(1080, 1350), (1080, 1920), (1080, 1080)] async def _render(browser, path, out): t0 = time.time() page = await browser.new_page() try: await page.goto('file://' + os.path.abspath(path)) ad = await page.evaluate("window.AD") w, h, d = int(ad['width']), int(ad['height']), float(ad['duration']) assert (w, h) in SIZES, f"bad size {w}x{h}" assert 4 <= d <= 12, f"bad duration {d}" await page.set_viewport_size({'width': w, 'height': h}) p = subprocess.Popen(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'image2pipe', '-framerate', str(FPS), '-i', '-', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-preset', 'veryfast', '-crf', '21', out], stdin=subprocess.PIPE) for i in range(int(d * FPS)): await page.evaluate(f"window.renderFrame({i / FPS})") p.stdin.write(await page.screenshot(type='jpeg', quality=88)) p.stdin.close(); p.wait() if p.returncode != 0: raise RuntimeError('ffmpeg failed') return {'file': path, 'ok': True, 'secs': round(time.time() - t0, 1), 'size': f'{w}x{h}', 'dur': d} except Exception as e: return {'file': path, 'ok': False, 'err': str(e).splitlines()[0][:200]} finally: await page.close() async def render_all(outdir, par, files): from playwright.async_api import async_playwright os.makedirs(outdir, exist_ok=True) async with async_playwright() as pw: br = await pw.chromium.launch() sem = asyncio.Semaphore(par); res = [] async def one(f): async with sem: out = os.path.join(outdir, os.path.basename(f).replace('.html', '.mp4')) r = await _render(br, f, out) print(json.dumps(r), flush=True); res.append(r) await asyncio.gather(*[one(f) for f in files]) await br.close() print(f"RENDERED {sum(r['ok'] for r in res)}/{len(res)}", flush=True) def ff(*args): subprocess.run(['ffmpeg', '-nostdin', '-loglevel', 'error', '-y', *args], check=True) def ads_mp4(): return sorted(f for f in glob.glob(os.path.join(STUDIO, 'mp4', '*.mp4')) if not os.path.basename(f).startswith('hero')) def sheets(): tdir = os.path.join(STUDIO, 'thumbs'); os.makedirs(tdir, exist_ok=True) files = ads_mp4() def thumb(f): ff('-ss', '4', '-i', f, '-frames:v', '1', '-vf', 'scale=300:300:force_original_aspect_ratio=decrease,pad=300:300:(ow-iw)/2:(oh-ih)/2:color=0x222222,' "drawtext=text='%s':x=6:y=6:fontsize=18:fontcolor=white:box=1:boxcolor=black@0.6" % os.path.basename(f)[:-4], os.path.join(tdir, os.path.basename(f).replace('.mp4', '.png'))) with ThreadPoolExecutor(8) as ex: list(ex.map(thumb, files)) pngs = sorted(glob.glob(os.path.join(tdir, '*.png'))) for n in range(0, len(pngs), 20): lst = os.path.join(tdir, f'list{n}.txt') open(lst, 'w').write(''.join(f"file '{p}'\n" for p in pngs[n:n + 20])) ff('-f', 'concat', '-safe', '0', '-i', lst, '-vf', 'tile=5x4', '-frames:v', '1', os.path.join(tdir, f'sheet_{n // 20 + 1:02d}.png')) print(f"SHEETS {len(range(0, len(pngs), 20))} in {tdir}") def wall(): files = ads_mp4()[:64] cdir = os.path.join(STUDIO, 'logs', 'cells'); os.makedirs(cdir, exist_ok=True) W, H = 134, 168 def cell(a): i, f = a ff('-i', f, '-vf', f'scale={W}:{H}:force_original_aspect_ratio=decrease:force_divisible_by=2,' f'pad={W}:{H}:(ow-iw)/2:(oh-ih)/2:color=0x0E1116,tpad=stop_mode=clone:stop_duration=8,fps=24', '-t', '8', '-an', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', os.path.join(cdir, f'c{i:02d}.mp4')) with ThreadPoolExecutor(8) as ex: list(ex.map(cell, enumerate(files))) while len(files) < 64: # repeat to fill the grid if fewer than 64 ads files.append(files[len(files) % max(1, len(files))]) n = len(glob.glob(os.path.join(cdir, 'c*.mp4'))) rows = [] for r in range(8): args = [] for c in range(8): args += ['-i', os.path.join(cdir, f'c{(r * 8 + c) % n:02d}.mp4')] row = os.path.join(cdir, f'row{r}.mp4'); rows.append(row) ff(*args, '-filter_complex', 'hstack=inputs=8', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', row) args = [] for row in rows: args += ['-i', row] ff(*args, '-filter_complex', 'vstack=inputs=8,pad=1080:1350:4:3:color=0x0E1116', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-pix_fmt', 'yuv420p', os.path.join(STUDIO, 'wall.mp4')) # gallery plan = {} pf = os.path.join(STUDIO, 'plan.md') if os.path.exists(pf): for line in open(pf): parts = [p.strip() for p in line.strip('- \n').split('|')] if len(parts) >= 5: plan[parts[0]] = parts cards = [] for f in ads_mp4(): i = os.path.basename(f)[:-4]; p = plan.get(i, [i, '', '', '', '']) cards.append(f'<figure><video src="mp4/{i}.mp4" muted loop autoplay playsinline></video>' f'<figcaption><b>{html.escape(i)}</b> · {html.escape(p[1])} · {html.escape(p[2])}<br>{html.escape(p[4])}</figcaption></figure>') open(os.path.join(STUDIO, 'gallery.html'), 'w').write( '<!doctype html><meta charset="utf-8"><title>Haiku Ad Studio gallery</title><style>' 'body{margin:0;background:#0e1116;color:#e8e8e8;font:14px -apple-system,Arial,sans-serif;padding:24px}' 'h1{font-size:22px}main{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}' 'figure{margin:0;background:#1a2029;border-radius:10px;overflow:hidden}video{width:100%;display:block;background:#000}' 'figcaption{padding:8px 10px;line-height:1.4}</style>' f'<h1>Haiku Ad Studio · {len(cards)} ads</h1><main>' + ''.join(cards) + '</main>') print('WALL studio/wall.mp4 + studio/gallery.html') if __name__ == '__main__': if sys.argv[1] == '--sheets': sheets() elif sys.argv[1] == '--wall': wall() else: asyncio.run(render_all(sys.argv[1], int(sys.argv[2]), sys.argv[3:])) ```
If something it needs isn't on your computer yet, Claude tells you the one line to install it and asks before it runs anything.
Approve the first ad
About ten minutes in, Claude stops and shows you one finished ad with three stills from it. Tell it what to change, or tell it to go. Nothing gets made at scale until you say yes.
Open your gallery
When the run finishes, Claude leaves a folder called studio on your computer and a short report in the chat.
gallery.html
every ad playing in a grid, with its angle and hook. Open it in your browser and pick.
wall.mp4
64 ads playing at once, for showing your team or a client.
Top 10 and test plan
the ads Opus rates highest and how to test them.
brand.md
the brand file. Every fact your ads use is listed there with its source page.
Make it yours
Running it for clients
Run it once per client in a separate folder. The brand file becomes the client's brand sheet, and the gallery is your review link.
Launching a product
Use the launch page as your website and put the launch date in brand notes. 100 ads is enough to refresh every week of the launch.
Selling in another language
Write "make every ad in German" (or any language) in brand notes. The fact checks stay the same.