Skip to content

Serve a rendered ad into a streaming break - #302

Merged
ralyodio merged 1 commit into
masterfrom
ads-decision
Sep 24, 2026
Merged

ralyodio merged 1 commit into
masterfrom
ads-decision

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The first piece of actually serving these ads to listeners. Adds GET /api/ads/stream — what a player calls when it wants something to play in a break — and the lookup that turns a chosen creative into a file to fetch.

Selection is not reinvented

The auction and the impression are serveAd's, exactly as they are for a banner. A second selection path would be a second set of numbers, and the one not wired to billing is the one that quietly gives inventory away. This route only answers the question serveAd cannot: given the creative it picked, which file should this player load?

Audio by default

nixamp — which is where this is headed — is a music player that already fetches an endpoint of this shape from its ad-break handler, and it has nowhere to put a picture. Handing it a <video> URL would be worse than handing it audio it can certainly play.

A video request gets 720p, not the 1080p master: the master is the download an advertiser keeps, and making a phone fetch it to watch five seconds spends their data on pixels the screen cannot show.

Only what was approved

Only published_revision is servable, with no fallback to another revision or another profile. A revision becomes published when the worker has validated it; serving the newest instead would put media on air that was never approved, and a silent fallback would do it without saying so.

An unfilled break is not an error

It returns 200 with a null url. A break nobody can fill simply does not happen and the listener keeps their content — the only safe default when the alternative is dead air. Every failure path, including exceptions, returns that.

The one case that is logged: a creative that won the auction and has no media. That's a campaign winning inventory it cannot fill, which is worth knowing about.

Not yet wired to anything

nixamp's client calls /api/ads/next and no route implements it — so today its breaks fetch, 404, and silently play nothing. That bridge is the next piece, along with a slot for the property.

Verification

6 tests covering the two real risks: never serving an unpublished revision, and giving each player the right profile. 2596 passed / 1 failed repo-wide — the pre-existing tracker-geo failure. Typechecks clean.

Adds the endpoint a player calls when it wants something to play in an ad
break, and the lookup that turns a chosen creative into a file to fetch.

Selection, the auction and the impression are not redone. They are serveAd's,
exactly as they are for a banner, because a second selection path would be a
second set of numbers and the one not wired to billing is the one that quietly
gives inventory away. This only answers the question serveAd cannot: given the
creative it picked, which file should this player load?

Audio by default. The properties most likely to call this are music and radio
players — nixamp already fetches an endpoint of this shape from its ad break
handler — and handing a video URL to something with nowhere to show a picture
is worse than handing it audio it can certainly play. A video request gets the
720p rendition rather than the 1080p master: the master is the download an
advertiser keeps, and making a phone fetch it to watch five seconds spends
their data on pixels the screen cannot show.

Only published_revision is servable, and there is no fallback to another
revision or another profile. A revision becomes published when the worker has
validated it; serving the newest instead would put media on air that was never
approved, and a silent fallback would do it without saying so.

An unfilled break returns 200 with a null url rather than an error. A break
nobody can fill simply does not happen and the listener keeps their content,
which is the only safe default when the alternative is dead air. The one case
that is logged is a creative that won the auction and has no media: that is a
campaign winning inventory it cannot fill, which is worth knowing about.

Nothing is wired to a property yet. nixamp's client calls /api/ads/next and no
route implements it; that bridge is the next piece.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

48 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 7b67d69 into master Sep 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant