Podcast Ad Slots: Dynamic Insertion, Pacing and Make-Goods
MediumAI-assisted coding · AI-Assisted Debugging
Podcast Ad Slots: Dynamic Insertion, Pacing and Make-Goods
You've joined the dynamic-ad-insertion team at a podcast network. When a listener downloads an episode, the ad server decides which ad creatives get stitched into the episode's ad breaks. Marketing has an incident open, a launch behind it, and a contract change behind that. All times are integer seconds.
Codebase
| File | What it does |
|---|---|
adserver/api.py | run(operations) entry point and the Dispatcher that routes each operation |
adserver/server.py | AdServer: the facade the API calls (request, load_test, ...) |
adserver/catalog.py | Catalog: episodes and campaigns, with input validation |
adserver/filler.py | SlotFiller: picks the creatives for each break of one request |
adserver/ranking.py | rank_key: the order in which campaigns get slots |
adserver/targeting.py | Is a campaign live, and does it target this show and country? |
adserver/impressions.py | ImpressionLog: who heard what and when (frequency caps and delivery counts) |
adserver/stitcher.py | Turns the chosen creatives into start times in the delivered file |
adserver/models.py | Episode, Marker, Campaign, Creative, Placement |
adserver/config.py, adserver/errors.py | Constants and error codes |
tests/ | Existing unit and end-to-end tests (one of them currently fails) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh ad server and returns one result per operation. Every operation is [name, ts, ...args]; ts is an integer and never decreases from one operation to the next. Errors are returned, never raised, as {"error": "<code>"}.
| Operation | Arguments after ts | Returns | Errors (checked in this order) |
|---|---|---|---|
add_episode | episode_id, show, duration, markers | true | invalid episode, episode exists |
add_campaign | campaign_id, spec | true | invalid campaign, campaign exists |
request | listener, episode_id, country | the stitched ad timeline | invalid arguments, episode not found |
load_test | episode_id, country, listeners, count, step | summary dict (load harness) | invalid arguments, episode not found |
- An unknown operation name returns
{"error": "unknown operation"}. A wrong number of arguments or a non-integertsreturns{"error": "invalid arguments"}. - Episode.
markersis a list of[offset, max_len]: an ad break atoffsetseconds into the *original* audio that may hold at mostmax_lenseconds of ads. Offset0is a pre-roll andoffset == durationa post-roll. An episode is invalid unlessepisode_idandshoware non-empty strings,duration > 0, there is at least one marker, every offset is in0..duration, offsets are strictly increasing, and everymax_len > 0. - Campaign
specis a dict withadvertiser(string),priority(int, higher is served first),showsandcountries(lists of strings; an empty list means "every show/country"),startandend(ints),goal(impressions bought),freq_cap(max impressions per listener in a rolling 7 days) andcreatives(a non-empty list of[creative_id, seconds]; creative ids are unique across the whole catalog). A campaign is *live* forstart <= ts < end. It is invalid unless every field is well-typed,start < end,end > tsof the operation that adds it,goal > 0,freq_cap > 0and every creative hasseconds > 0. Unknown keys are ignored. requestis one podcast download bylistener(non-empty string) incountry(non-empty string, case-sensitive, e.g."US"). It returns[[start_second, creative_id], ...]: every ad that will play, in playing order, at its position in the stitched file. Empty if nothing qualifies.load_testreplayscountrequests for you so tests don't need thousands of literal operations: requesti(from 0) runs atts + i * stepfor listener"L<i mod listeners>"and the op returns{"requests", "ads", "digest"}. It is already implemented; just make sure it stays fast.
How a request is filled
- Candidates. Campaigns that are live at
tsand whoseshows/countriesmatch the episode's show and the listener's country. - Breaks are filled in offset order. Within one break, repeat: walk the candidates in rank order and take the first one that (a) has no creative of the same advertiser already in this break (two campaigns of one advertiser count as the same advertiser), (b) is under its frequency cap, and (c) has a creative that still fits in the break's remaining seconds. Place that campaign's longest creative that fits (ties: smaller creative id), shrink the remaining room, and start over. The break is done when no candidate qualifies. A campaign can place at most one creative per break, but may appear in several breaks of the same request.
- Rank order (
adserver/ranking.py): higherpriorityfirst, then smallercampaign id(plain string order). - Frequency cap. A listener's impressions of a campaign count while
ts - impression_ts < 604800(exactly seven days after an impression it no longer counts). A campaign withcount >= freq_capis skipped. An ad placed earlier in this same request is an impression with this request'sts. - Stitching. Ads in a break play back to back from the break's offset. Each ad's
start_secondis its break'soffsetplus the total length of every ad inserted before it (in earlier breaks and earlier in its own break), because each inserted second pushes the rest of the episode later.
Worked example. Episode ep-1 has breaks [[0, 30], [600, 60], [1800, 30]]. bolt runs two campaigns, nova one (all with shows: [], countries: [], start: 0, end: 8640000, goal: 100000):
| campaign | advertiser | priority | freq_cap | creatives |
|---|---|---|---|---|
c-bolt-a | bolt | 9 | 1 | bolt-20 (20 s), bolt-40 (40 s) |
c-bolt-b | bolt | 8 | 5 | bolt-b25 (25 s) |
c-nova | nova | 5 | 3 | nova-15 (15 s), nova-30 (30 s) |
["request", 100, "u1", "ep-1", "US"] returns [[0, "bolt-20"], [620, "bolt-b25"], [645, "nova-30"], [1875, "bolt-b25"]]:
- Break 1 (30 s):
c-bolt-atakesbolt-20(bolt-40does not fit);c-bolt-bis bolt again, andnova-15does not fit in the remaining 10 s. - Break 2 (60 s) starts at
600 + 20:c-bolt-ahas reached its cap of 1 in this very request, soc-bolt-bplacesbolt-b25, thennova-30at620 + 25. - Break 3 (30 s) starts at
1800 + 20 + 25 + 30 = 1875:bolt-b25fits; nothing else fits in the last 5 s.
Part 1: Incident AD-4417, listeners are hearing the wrong ads
Goal: make the inserter follow the rules above. Sales and listener support have reported:
- "I heard the same car ad three times in one episode even though the campaign's cap is 1 per week."
- "Two Bolt ads played back to back in one break, from different Bolt campaigns."
- "The post-roll starts in the wrong place in episodes where the pre-roll *and* a mid-roll both got ads, and I can hear the mid-roll ad cut into the content."
- "A campaign was still serving at the exact second its end date hit, and a listener who heard a capped ad exactly seven days ago was still blocked."
tests/test_filler.py::test_timeline_is_shifted_by_every_earlier_ad currently fails. Fix the causes, not the symptoms; hidden tests probe every rule in "How a request is filled".
Part 2: Goal pacing and a launch-day load (AD-4460)
Goal: same priority no longer means "alphabetical". Campaigns now have a delivery goal that the ad server must pace, and the new report operation shows progress. The launch will hammer the server.
| Operation | Arguments after ts | Returns | Errors |
|---|---|---|---|
report | campaign_id | {"delivered": n, "goal": g} | campaign not found |
delivered is the number of impressions of that campaign across all listeners so far.
Rules
- The rank order becomes: higher
priority, then the campaign furthest behind its goal pace (largestbehind), then smaller campaign id, wherebehind = expected - deliveredandexpected = goal * (ts - start) // (end - start)(integer floor division;tsis the request's timestamp).behindmay be negative. deliveredincludes impressions placed earlier in the same request: the ranking is re-evaluated every time a creative is placed, in every break.- A campaign whose
delivered >= goalis no longer a candidate. - Hidden tests replay thousands of requests (thousands of placements) through
load_testfrom a pool of listeners. A solution that rescans the whole impression history for every check will not finish in time.
Example. One break of 30 s; campaigns ca (priority 1, goal 10, start 0, end 100) and cb (priority 1, goal 10, start 0, end 75), each with one 30-second creative (ca-30, cb-30), freq_cap 9. At ts = 50: ca expects 10*50//100 = 5, cb expects 10*50//75 = 6. Three requests at ts = 50 from u1, u2, u3 return cb-30, ca-30, cb-30 (after cb's first impression both are 5 behind, so the smaller id ca wins), and ["report", 50, "ca"] returns {"delivered": 1, "goal": 10}.
Part 3: Competitive separation and make-goods (AD-4502)
Goal: two contract changes from the sales team. Earlier behaviour must keep working for campaigns that use neither feature.
Rules: competitive separation
- A campaign
specmay carry an optionalcategory(non-empty string; absent ornullmeans no category; any other type makes the campaign invalid). Two campaigns of different advertisers with the same category must never both appear in one request, across all breaks of the episode. - In a request, the first ad placed from a categorised campaign (breaks in offset order, placements in order) *claims* that category for its advertiser. Later in that request, campaigns of the same category from any other advertiser are skipped like any other ineligible candidate; the claiming advertiser can still place more ads (the one-per-advertiser-per-break rule still applies). Only an actual placement claims a category: a candidate that was skipped, capped or didn't fit claims nothing. Claims last for that request only.
Rules: make-goods
- When
ts >= endof a campaign and its shortfallgoal - deliveredis positive, a *make-good* campaign exists forend <= ts < end + 14*86400(14 days; the end is exclusive). It has the same advertiser, creatives,shows,countries,categoryandfreq_capas the original, a goal equal to the shortfall, its own delivery count and its own frequency counts (the original's impressions never count toward the make-good's cap). It stops once it has delivered its shortfall. Nothing makes good for a make-good. - Make-goods rank ahead of every regular campaign, regardless of priority or pace; among make-goods, the one whose original ended earlier goes first, then the smaller original id. They follow every other rule (same-advertiser-per-break, category claims, caps, fitting).
reportfor a campaign keepsdeliveredandgoalfor the original only, and adds"makegood": {"goal": shortfall, "delivered": n}wheneverts >= endand the shortfall is positive (even after the 14 days are over). Otherwise there is nomakegoodkey.
Example: separation. Break [0, 30] and [600, 30]; c-volt (volt, priority 9, category auto, freq_cap 1, volt-30), c-zoom (zoom, priority 8, category auto, zoom-30) and c-kibble (kibble, priority 1, no category, kib-30). A request returns [[0, "volt-30"], [630, "kib-30"]]: volt claims auto in the first break, its cap stops it in the second, and zoom stays blocked.
Example: make-good. One 30-second break; c-old (old, priority 9, goal 3, start 0, end 100, freq_cap 1, old-30) and c-new (new, priority 1, goal 100000, new-30). Requests: u1 at 10 and 20, u1 at 100 and 101, u2 at 102, u3 at 103. They return old-30, new-30 (cap), then at 100 old-30 (c-old has ended with a shortfall of 2, and the make-good outranks c-new), new-30 (the make-good's cap for u1), old-30 (u2, the shortfall is now fully delivered), new-30. ["report", 103, "c-old"] returns {"delivered": 1, "goal": 3, "makegood": {"goal": 2, "delivered": 2}}.
Notes
models.py,config.py,errors.pyandcatalog.pyvalidation are known-good apart from what the parts above ask you to extend.- You may use the AI assistant freely; you are expected to verify what it produces.
- Later parts must not break earlier ones; hidden tests re-check earlier behaviour.
Example 1
- Input:
- [["add_episode", 1, "ep-1", "tech-talk", 1800, [[0, 30], [600, 60], [1800, 30]]], ["add_campaign", 2, "c-bolt-a", {"advertiser": "bolt", "priority": 9, "shows": [], "countries": [], "start": 0, "end": 8640000, "goal": 100000, "freq_cap": 1, "creatives": [["bolt-20", 20], ["bolt-40", 40]]}], "...c-bolt-b (bolt, priority 8, bolt-b25 25s), c-nova (nova, priority 5, nova-15, nova-30)...", ["request", 100, "u1", "ep-1", "US"]]
- Output:
- [... , [[0,"bolt-20"],[620,"bolt-b25"],[645,"nova-30"],[1875,"bolt-b25"]]]
- Explanation
- Bolt's two campaigns never share a break, c-bolt-a is capped after its first placement in this same request, and every start time is shifted by the ads inserted before it.
Example 2
- Input:
- [["add_episode", 1, "ep-1", "tech-talk", 600, [[0, 30]]], "...c-old (goal 3, end 100, freq_cap 1, priority 9), c-new (priority 1)...", ["request", 10, "u1", "ep-1", "US"], ["request", 20, "u1", "ep-1", "US"], ["request", 100, "u1", "ep-1", "US"], ["request", 101, "u1", "ep-1", "US"], ["request", 102, "u2", "ep-1", "US"], ["request", 103, "u3", "ep-1", "US"], ["report", 103, "c-old"]]
- Output:
- [..., [[0,"old-30"]], [[0,"new-30"]], [[0,"old-30"]], [[0,"new-30"]], [[0,"old-30"]], [[0,"new-30"]], {"delivered":1,"goal":3,"makegood":{"goal":2,"delivered":2}}]
- Explanation
- c-old ended with 1 of 3 delivered, so a make-good for the remaining 2 outranks c-new from ts 100 on, with its own frequency cap per listener.
Guidelines
- Python 3 standard library only
- All times are integer seconds and never decrease from one operation to the next
- Every operation returns exactly one value; errors are returned as {"error": <code>}, never raised
- Part 2 inputs replay thousands of requests (thousands of placements) through load_test