diff --git a/docs/superpowers/specs/2026-04-15-server-side-ad-templates-design.md b/docs/superpowers/specs/2026-04-15-server-side-ad-templates-design.md new file mode 100644 index 00000000..454f3764 --- /dev/null +++ b/docs/superpowers/specs/2026-04-15-server-side-ad-templates-design.md @@ -0,0 +1,363 @@ +# Server-Side Ad Templates Design + +*April 2026* + +--- + +## 1. Problem Statement + +Today's display ad pipeline on most publisher sites is structurally sequential +and browser-bound: + +1. Page HTML arrives at browser +2. Prebid.js (~300KB) downloads and parses +3. Smart Slots SDK scans the DOM to discover ad placements +4. `addAdUnits()` registers slot definitions +5. Prebid auction fires from the browser (~80–150ms RTT to SSPs) +6. Bids return (~1,000–1,500ms window) +7. GPT `setTargeting()` + `refresh()` fires +8. GAM creative renders + +**Total time to ad visible: ~3,100ms.** + +The browser is the slowest possible place to run an auction. It must first download and parse +multiple SDKs, scan the DOM to discover what ad slots exist, and then fire SSP requests over +a consumer internet connection with high and variable latency. + +Trusted Server sits at the Fastly edge — milliseconds from the user, with data-center-to-data-center +RTT to Prebid Server (~20–30ms vs ~80–150ms from a browser). The server knows, from the request +URL alone, exactly which ad slots are available on any given page. There is no reason to wait for +the browser. + +--- + +## 2. Goal + +Enable Trusted Server to: + +1. Match an incoming page request URL against a set of pre-configured slot templates +2. Immediately fire the full server-side auction (all providers: PBS, APS, future wrappers) in + parallel with the origin HTML fetch — before the browser receives a single byte +3. Inject GPT slot definitions into `` so the client can define slots without any SDK +4. Return pre-collected winning bids to the browser's lightweight `/auction` POST before the + browser would have even finished parsing Prebid.js +5. Eliminate Prebid.js from the client entirely + +**Target time to ad visible: ~1,200ms. Net saving: ~2,000ms.** + +--- + +## 3. Non-Goals + +- Eliminating client-side GPT / Google Ad Manager — GAM remains in the rendering pipeline + for Phase 1. The GAM call (`securepubads.g.doubleclick.net`) moves server-side in a future phase. +- Dynamic slot discovery (reading the DOM) — this design commits to pre-defined, URL-matched + slot templates. Smart Slots' dynamic injection behavior is replaced by server knowledge. +- Changing the `AuctionOrchestrator` internally — the orchestrator already handles parallel + provider fan-out. This design adds a new trigger point, not new auction logic. + +--- + +## 4. Architecture + +### 4.1 New File: `creative-opportunities.toml` + +A new config file at the repo root, alongside `trusted-server.toml`. It holds all slot templates: +page pattern matching rules, ad formats, floor prices, and GAM targeting key-values. Bidder-level +params (placement IDs, account IDs) live in Prebid Server stored requests, keyed by slot ID — not +in this file. + +Loaded at build time via `include_str!()`, parsed into `Vec` at startup. +Ad ops can edit this file independently of server configuration. + +`floor_price` is the publisher-owned hard floor per slot — the source of truth for the minimum +acceptable bid price, enforced at the edge before bids reach the ad server. Any bid below the +floor is discarded at the orchestrator level before it enters `__ts_bids`. SSPs may apply their +own dynamic floors independently within their platforms; this floor is the publisher's baseline +that supersedes all other floor logic by virtue of being enforced earliest in the pipeline. + +**Schema:** + +```toml +[[slot]] +id = "atf_sidebar_ad" +page_patterns = ["/20*/"] +formats = [{ width = 300, height = 250 }] +floor_price = 0.50 + +[slot.targeting] +pos = "atf" +zone = "atfSidebar" + +[[slot]] +id = "below-content-ad" +page_patterns = ["/20*/"] +formats = [{ width = 300, height = 250 }, { width = 728, height = 90 }] +floor_price = 0.25 + +[slot.targeting] +pos = "btf" +zone = "belowContent" + +[[slot]] +id = "ad-homepage-0" +page_patterns = ["/", "/index.html"] +formats = [{ width = 970, height = 250 }, { width = 728, height = 90 }] +floor_price = 1.00 + +[slot.targeting] +pos = "atf" +zone = "homepage" +slot_index = "0" +``` + +**Rust type:** + +```rust +#[derive(Debug, Clone, serde::Deserialize)] +pub struct CreativeOpportunitySlot { + pub id: String, + pub page_patterns: Vec, + pub formats: Vec, + pub floor_price: Option, + pub targeting: HashMap, +} +``` + +### 4.2 URL Pattern Matching + +At request time, TS matches the request path against each slot's `page_patterns`. Patterns are +glob-style strings: + +- `/20*/` — matches all date-prefixed article paths (e.g., `/2024/01/my-article/`) +- `/` — matches the homepage exactly +- `/index.html` — exact match + +Multiple slots can match a single URL. All matching slots are collected and fed into a single +auction as separate impressions. Pattern matching is purely in-memory against the pre-parsed +config — sub-millisecond. + +### 4.3 Auction Trigger + +When slots are matched, TS immediately calls `AuctionOrchestrator::run_auction()` with the +matched slots converted to `AdSlot` objects. This happens at request receipt time — in parallel +with the origin fetch. + +The orchestrator's existing behaviour is unchanged: +- All providers (PBS, APS, any configured wrappers) are dispatched simultaneously +- Per-provider timeout budgets are enforced from the remaining auction deadline +- Floor price filtering, bid unification, and winning bid selection are applied as today +- PBS resolves bidder params from its stored requests by slot ID — no bidder params travel + through TS or the browser + +**On NextJS 14 (buffered mode):** TS must buffer the full origin response before forwarding. +This gives the auction the entire origin response time (~150–400ms typical) to run before +any HTML is forwarded. In practice, bids are often collected before origin even responds. + +**On NextJS 16 (streaming mode):** TS streams HTML chunks to the browser immediately. The +auction runs in parallel. Bid injection into `` must complete before the `` tag +is forwarded. If the auction has not returned by the time `` is encountered, TS waits +up to the remaining auction budget, then flushes with whatever bids have arrived (partial +results) or no targeting if timed out. Content after `` is never held. + +### 4.4 Head Injection + +TS injects two separate `