8Examples / blog
Marketing · AI-assisted workflow

An Eight-Second Ad, Rendered From Code

I needed an eight-second ad for CargoPax to run above a link in a social feed. I did not open a video editor. I opened the Codex CLI in the repository and asked for one.

By Sean Bennett · September 3, 2026 · 7 min read
The result: eight seconds, square, silent, 1.1 MB. It loops here; in a feed it plays once above the link.

CargoPax is a small product with one idea: forward your shipping emails to your own [email protected] address and every parcel gets tracked for you and your team, across UPS, FedEx, USPS, DHL, Canada Post, and Purolator. An ad for it has to land that idea in the time a thumb takes to scroll past. Eight seconds, four beats, no sound, because feeds autoplay muted.

What I asked for

The whole brief fit in one message. An 8-second ad video to sell cargopax.ca, show the features, push people to sign up. Square, because the link sits above the video in a feed post. Silent. And one constraint that decided everything downstream: make it reproducible. I wanted to change a line of copy and re-render, not re-edit.

A Codex CLI session in the CargoPax repository: the prompt asking for an eight-second square silent ad, the four-step plan Codex proposed, and the files it explored and edited
The session, recreated from the transcript. The plan came back in seconds: render it rather than edit it, and share the brand mark with the icons.

That last constraint is why the answer was a script and not a timeline. A video editor produces a file. A script produces a file and a way to produce the next one. When the copy changes, or the price, or the carrier list, I edit a string and run it again.

How the render works

The script is scripts/render-promo.mjs in the CargoPax repository, about 370 lines. The core of it is one function, frame(t), that takes a time in seconds and returns an SVG string of what the screen should look like at that instant. Everything else is plumbing around that function.

  • Scenes are envelopes. A scene(t, enterAt, exitAt) helper returns an opacity and a vertical drift for a group: it fades and rises in over 0.45 s, holds, then fades and drifts up as it leaves. Each beat is one scene.
  • Motion is easing, not keyframes. Small helpers such as easeOut, easeIn, and an easeBack that overshoots and settles give the email cards their drop and the address chip its bump when each card lands in it.
  • Sharp rasterises, ffmpeg encodes. Each SVG becomes a PNG through sharp, eight frames at a time. Then a single ffmpeg call strings 240 frames into H.264 at 30 fps.
  • The mark is shared. The parcel-with-a-pin logo lives in scripts/brand-mark.mjs and is drawn by the icon generator and by the video from the same code, so the favicon and the end card are the same object.
// A scene's group: fades and rises in, fades and drifts out.
function scene(t, enterAt, exitAt, { enterFor = 0.45, exitFor = 0.35 } = {}) {
  const enter = span(t, enterAt, enterAt + enterFor);
  const exit = 1 - span(t, exitAt, exitAt + exitFor, easeIn);
  const visible = enter * exit;
  const dy = lerp(28, 0, enter) + lerp(0, -20, 1 - exit);
  return { visible, wrap: inner => (visible <= 0 ? '' : `<g opacity="${visible}" transform="translate(0 ${dy})">${inner}</g>`) };
}

// yuv420p and an even size are what every player wants; faststart puts
// the index first so it streams from the site instead of downloading.
execFileSync(process.env.FFMPEG || 'ffmpeg', [
  '-y', '-framerate', '30', '-i', 'frame-%04d.png',
  '-c:v', 'libx264', '-preset', 'slow', '-crf', '19',
  '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', 'public/promo.mp4',
]);

The script also has a --keyframes mode that renders twelve moments to a contact sheet instead of a video. That is the cheap check. A full render takes a couple of minutes; the sheet takes seconds and tells you whether the timing reads before you spend them.

A contact sheet of twelve frames from the CargoPax promo, from the email pile-up through the address chip, the shared dashboard, and the end card
The contact sheet at 0.3, 1.2, 2.0, 2.5, 3.0, 3.8, 4.8, 5.4, 6.0, 6.6, 7.2, and 7.9 seconds. This is what I looked at before the full render.

The four beats

Eight seconds is enough for one problem, one action, one payoff, and one ask. The storyboard is written into the script header as timings, and each line is one scene:

  1. 0.0 to 2.3 s, "Shipping emails pile up." Three email cards drop in, from a supplier, a parts vendor, and Canada Post, each with a tracking link in the body. The problem.
  2. 2.2 to 4.4 s, "Forward them to your own address." The cards fly into a chip that reads [email protected], the chip bumps as each one lands, a green check appears, and the line under it says "Nothing to copy. No extension." The action.
  3. 4.3 to 6.3 s, "Every parcel, tracked. Shared with your whole team." A dashboard for a fictional Acme Tools slides its rows in, and the Canada Post row flips from In transit to Delivered with a push notification dropping in behind it. The payoff.
  4. 6.2 to 8.0 s, the end card. The small lockup that sat in the corner the whole time grows into the mark and the wordmark, then "Create your account at cargopax.ca" and an arrow that bobs toward the link above the post. The ask.
Frame from the third beat: the Acme Tools dashboard with four parcels, one row flipped to Delivered, and a CargoPax notification toast
5.7 seconds in. The moment the ad is built around: one parcel ticks over, and the team hears about it.
The Codex CLI session continuing: the keyframes command, Codex's read of the contact sheet, the full render writing 240 frames to public/promo.mp4, an ffprobe check, and the commit
Contact sheet, render, probe, commit. Recreated from the transcript; the file names, timings, and output are the real ones.

Small decisions that mattered

Square, not wide. A feed post shows the link above the media. A 1080 by 1080 clip is the largest thing that fits without cropping on the platforms this runs on, and the last beat can point up at the link and mean it.

Silent on purpose. Feeds autoplay muted. A clip that needs sound to make sense is a clip that does not make sense. Every beat has its headline on screen.

Text metrics are guessed. There is no font measurement available when you are writing an SVG string, so the pill widths are computed from an average glyph width times the label length. It is close enough for the labels used, and it is one of the reasons the contact sheet exists.

faststart. The MP4 index goes at the front of the file, so the clip starts playing on the site before it has finished downloading. That flag is the difference between a video that plays and a video that spins.

The mark is code. Because the parcel is drawn by a function, the favicon, the app icons, the social card, and the last frame of the ad are guaranteed to match, and the icon generator still produces byte-identical output after the refactor. That was checked, not assumed.

What this costs and what it buys

The whole thing was one session: a prompt, a plan, a script, a contact sheet, a render, a commit. There is no project file to lose and no editor to reinstall. Next month, when the copy changes, the video changes with it, and the diff is a line of text in a code review rather than a new export nobody can compare to the old one.

If you run a small product and you have been putting off the promo video because the tooling is a chore, this is the shape of the shortcut. Write down what the eight seconds should say. Let the script draw it.

Comments 0

No comments yet. Start the conversation.

Leave a comment

Site author? Sign in to reply officially.

Commenting is temporarily unavailable while CAPTCHA is being configured.