From c5c509de9ea8ce818cc4f60cf5f5eb64bff69993 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 24 Sep 2026 15:35:05 +0000 Subject: [PATCH] Queue the pre-roll for API-created campaigns too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Queueing the render lived only in the dashboard server action, but that is not the only way a campaign is created. The public API at /api/ads/v1/campaigns goes through createCampaignForUrl in lib/ads/campaigns.ts, and that is how myna and the rest of the automation file ads. Every automatically created campaign therefore got its display creatives and no video, and nothing anywhere said so — the campaign simply had no pre-roll, forever. It showed up as a campaign created at 15:27 today, after the backfill, sitting with six design creatives and no render job while every backfilled campaign had one. Moves the call into the shared creator so both paths are covered by construction rather than by remembering. Same rule as the trending promo grant directly below it: a render is an extra output of creating a campaign, never a precondition for one, and queueCampaignVideo returns null on every failure rather than throwing. Co-Authored-By: Claude Opus 5 (1M context) --- lib/ads/campaigns.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/ads/campaigns.ts b/lib/ads/campaigns.ts index f5ef0f9..1fdc752 100644 --- a/lib/ads/campaigns.ts +++ b/lib/ads/campaigns.ts @@ -18,6 +18,7 @@ export { parseCampaignRequest, parseCampaignPatch, isRefSlug, domainOf, type Cam import { getOrCreateDefaultOrg } from "@/lib/orgs"; import { generateAdCreatives, cleanSummary, creativesFromCopy, templateCopy, summaryDomain, type AdCreative, type AdSummary } from "@/lib/ads/creative"; import { extractSiteBrand, type SiteBrand } from "@/lib/ads/brand"; +import { queueCampaignVideo } from "@/lib/ads/video/jobs"; import { DEFAULT_BID_CREDITS } from "@/lib/ads/pricing"; import { cleanTopics, promoState, type PromoState } from "@/lib/ads/trending"; import { grantTrendingPromo, promoForCampaign, promosForCampaigns } from "@/lib/ads/promos"; @@ -262,6 +263,34 @@ export async function createCampaignForUrl(input: { return { ok: false, status: 500, error: creativeError.message }; } + // Queue the five-second pre-roll. This lives here rather than only in the + // dashboard action because this is the shared creator: the public API at + // /api/ads/v1/campaigns runs through it, which is how myna and the rest of + // the automation file ads. With the queueing only in the server action, every + // automatically created campaign got display creatives and no video, and + // nothing said so — the campaign simply had no pre-roll forever. + // + // Same rule as the promo below: a render is an extra output of creating a + // campaign, never a precondition for one. queueCampaignVideo returns null on + // every failure rather than throwing. + await queueCampaignVideo(sb, { + campaignId: campaign.id, + ownerId: userId, + domain, + creatives: generated.creatives.map((c) => ({ + format: c.format, + headline: c.headline ?? "", + ctaText: c.ctaText ?? "", + bgColor: c.bgColor ?? null, + fgColor: c.fgColor ?? null, + accentColor: c.accentColor ?? null, + fontFamily: c.fontFamily ?? null, + logoUrl: c.logoUrl ?? null, + imageUrl: c.imageUrl ?? null, + })), + bumpRevision: false, + }); + // Turning trending targeting on is what earns the 90 days. Granted after the // campaign exists so the entitlement can name it, and a failure to grant // never fails the campaign: the ads still run, they simply bill normally,