Sibling of architecture-flow.html. The ad config is a tiny cfg/<id>.js on R2 loaded by a <script> tag → CORS-exempt, edge-cached, $0 Worker.
The baked POP_CONFIG.window.url decides the mode via /^https:\/\/cfg\.analyytics\.com\/.*\.js$/i: a plain landing url → the engine opens it directly, no cfg request (today's behaviour, backward compatible); a cfg.analyytics.com/<id>.js url → the wrapper loads that cfg via <script> at init, sets window.__ADCFG, and pickAdUrl() rotates (still R2-direct, $0). That conditional load is not yet baked into the wrapper. Triggers stay governed by POP_CONFIG; rotation only picks WHICH url.
cfg/<id>.js and purges that one URL — live in ~250 ms, no wrapper re-bake. Delete cascades across KV + both R2 keys for free.The panel reads with GET /admin/adconfig?id= → {id, cfg} and writes with PUT /admin/adconfig {id, cfg}, which validateCfg()-gates the payload server-side, writes window.__ADCFG=+JSON and purges → {ok:true, purged}. The purge reuses the wrapper zone + token CF_ZONE_ID, because cfg.analyytics.com lives in the analyytics.com zone — no separate CF_ENGINE_ZONE_ID.
| Event | Worker | KV | R2 | Purge | Billed? |
|---|---|---|---|---|---|
Pageview — wrapper /w/<id>.js | 1 | 0 hit / 1 miss | 0 | 0 | 1 Worker req — the ONLY billed item |
Pageview — engine e/<id>.js (R2 direct) | 0 | 0 | 0 edge-cached | 0 | $0 · free egress |
Pageview — ad config cfg/<id>.js NEW | 0 | 0 | 0 edge-cached | 0 | $0 · free egress |
Pop & rotation — pickAdUrl() | 0 | 0 | 0 | 0 | $0 · runs in browser |
URL change — PUT /admin/adconfig (operator, rare) | 1 admin | 0 | 1 Class-A write | 1 | $0 · ≪ free tiers |
| Delete wrapper (operator, rare) | 1 admin | 1 del | 2 del (e/ + cfg/) | 1 | $0 · DeleteObject free |
Storage — cfg/<id>.js ≈ 300 B × N wrappers | — | — | ≪ 10 GB free | — | $0 |
Verified CF rates (2026-07-04): R2 Class-B read $0.36/M (10M free/mo) · R2 Class-A write $4.50/M (1M free/mo) · R2 storage $0.015/GB-mo (10 GB free) · egress $0 · DeleteObject free. Rotation adds $0 incremental: the config is one more edge-cached .js served exactly like the engine.
1000 wrappers × ~250 colos × 30 ≈ 7.5M reads/mo < 10M free = $0. The cost lever.
The only lever for already-cached repeat visitors — browser caches are not purgeable. Costs nothing at the edge.
Freshness on new loads (~250 ms) without lowering Edge TTL. A short Edge TTL instead would explode R2 reads → up to ~$104/mo.
.js+<script> over .json+fetch, and the purge token (Option A).A <script> load is CORS-exempt and edge-cached by default. A fetch().json() plan would need R2 CORS configured, and .json is not edge-cached by default while .js is. Using .js removes both audit blockers at once.
The round-robin cursor must advance exactly once per pop. The engine may call the URL resolver a browser-dependent number of times, so compute the pick once and memoize per pop. Also delete the leftover getPopUrl() in beforeOpen, or the cursor double-steps.
Guard /admin/adconfig with an OPTIONS/preflight branch (→ 204), bearer auth (→ 401), and a 501 when the R2 ENGINE binding is absent. GET ?id= reads the stored cfg; PUT {id, cfg} validates + writes. Without the preflight branch the browser's CORS preflight fails and the update call never fires.
Because the config now lives on cfg.analyytics.com, inside the analyytics.com zone, the purge reuses the existing wrapper zone + token CF_ZONE_ID — no separate CF_ENGINE_ZONE_ID is needed. If a purge ever fails, propagation just degrades from ~250 ms to the Edge TTL (still correct).
If the config <script> hasn't loaded before pop #1, impression #1 uses the baked default URL; rotation kicks in from pop #2. Accepted resilience trade-off — the pop always fires.
Purge clears the edge, not browsers. A repeat visitor keeps the old config until their Browser TTL expires — hence keep Browser TTL short (≈ 60 s) so freshness is bounded.
Worst case 1000 wrappers × ~330 colos ≈ 9.9M Class-B reads/mo — just under the 10M free tier. Tiered Cache collapses colo fan-out so real reads stay well below that, keeping the read cost at $0.