⚑ Sangsakala Proxy 🏠 Home
Skip to content
rayning0Public

About

AdPod Lab: CTV ad decision engine that uses creative identity resolution, household frequency capping, + revenue-aware optimization to reduce repetitive ads across fragmented ad sources.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

AdPod Lab: Intelligent CTV Ad Pod Optimization

by Raymond Gan

AdPod Lab is a Go-based CTV ad-decisioning simulation. It finds ad identity across fragmented demand sources, constructs revenue-aware ad pods, and applies household frequency caps to reduce ad repetition.

The two-day MVP deliberately runs as one local Go process. JSON fixtures and a concurrency-safe in-memory store keep it easy to run, test, and reason about without external infrastructure.

The problem

CTV viewers can get the same ad multiple times in one ad break or repeatedly across channels and devices. Ad decisioning must balance viewer experience with incomplete creative identifiers, protected inventory, fixed pod duration, household exposure history, and competing CPMs.

Given ads from multiple demand sources, inconsistent creative identifiers, household viewing history, contractual restrictions, and competing CPMs, construct an ad pod that minimizes repetition while preserving revenue, fill rate, and low decision latency.

The MVP models these constraints with fixtures and measures local decision latency. Its single-process implementation doesn't claim production-scale latency or multi-instance correctness.

The simulator reports its tradeoffs rather than assuming them. Results are generated, not real production results.

MVP scope

Capability Two-day MVP
Runtime Go and the standard library
Candidate input JSON candidate, creative, and identity fixtures
Identity resolution Synthetic UniversalAdId/ACIF-style metadata, exact media hash, and fingerprint fixtures
Frequency state In-memory map protected by sync.Mutex
Concurrency Atomic reserve, confirm, and cancel operations within one process
Simulation Seeded viewing events, playback outcomes, and isolated baseline-versus-optimized replays
Metrics Selection, settlement, revenue, fill, duplicate, and canonical cap metrics
Verification Unit tests plus go test -race ./...

No Docker, Redis, external exchange, video processing, or streaming integration is required to build or run the MVP.

Architecture

                Seeded Viewing Simulator
                           |
                           v
             JSON Candidate Fixtures
                           |
                           v
                Creative Identity Resolver
                           |
                           v
                Ad Pod Decision Engine
        (protected inventory, CPM selection, deduplication)
                           |
                           v
            In-Memory FrequencyStore Interface
             map + sync.Mutex; atomic reservations
                           |
                  +--------+--------+
                  |                 |
                  v                 v
        confirm impression     cancel / expire reservation
                  |                 |
                  +--------+--------+
                           v
                Metrics and Comparison Report

The frequency store is accessed only through an interface. This keeps the optimizer independent of storage and makes a later shared-state implementation a contained change.

The simulator first creates one complete, read-only viewing history. It then plays that exact history twice, starting each run with separate exposure records. The baseline remembers ads separately for each device and trusts the ID supplied by each ad source. The optimized run recognizes when different source IDs represent the same commercial and shares that history across the household's devices. Both runs use the same candidate ads, simulated clock, and playback results, so the comparison changes the ad-selection rules rather than the viewing behavior.

type FrequencyStore interface {
    Reserve(ctx context.Context, householdID, creativeID string,
        limit int, window, ttl time.Duration) (reservationID string, ok bool, err error)
    Confirm(ctx context.Context, reservationID string, at time.Time) error
    Cancel(ctx context.Context, reservationID string) error
}

For the MVP, InMemoryFrequencyStore holds confirmed impressions and active reservations behind one mutex. Reserve prunes expired state, counts confirmed impressions plus active reservations in the rolling window, and records a reservation in the same critical section. This prevents cap oversubscription among goroutines in the single process.

Creative identity proof

These fields show different kinds of ad ID proof. They aren't interchangeable. In this project, all data are fake test data.

  • universal_ad_id is a source-independent creative ID. In this project, it's fake test data, not a true ACIF registry ID. UniversalAdId is the VAST element that gives an ad a single identity across every system that touches it. Ad servers, DSPs, SSPs, and verification vendors each assign their own internal creative IDs, but none match. The UniversalAdId is the one ID that means the same unique ad everywhere.
  • source_creative_id is assigned by one demand source (Amazon APS, Google Ad Manager, Publica, FreeWheel...). Different sources can assign different values to the same ad, so it's never enough by itself to prove canonical identity.
  • media_hash identifies the exact file bytes of a video with a cryptographic hashing algorithm (like SHA-256 or MD5). Test values use sha256:-style strings. If 2 video files have equal hashes, they are byte-for-byte identical files. Different hashes may represent different transcodes (720p vs. 1080p) or remuxed files of the same ad.
  • fixture_fingerprint: Audio/video fingerprinting creates unique mathematical signatures based on how media actually looks and sounds, not relying on file metadata or raw bits. This lets us ID ads even if their files are resized, compressed, or transcoded. Audio fingerprints could use Chromaprint. Video fingerprints could use VideoHash.

This resolver should use proof in descending order of trust: universal_ad_id -> exact media hash -> fixture_fingerprint -> unknown. A source-local ID, advertiser, or product match does not mean 2 ads are the same.

The fixture resolver loads data/identity_registry.json, a separate trusted mapping from opaque proof values to canonical IDs. It never derives an answer from a candidate's identifier text, and data/ground_truth.json is evaluation data only. This prevents the synthetic test data from leaking canonical IDs through strings such as fingerprint-creative-003.

Scenarios the resolver must distinguish

The current test fixtures keep the three identity signals separate. Check universal_ad_id for trusted identifier proof, media_hash for exact-file proof, and fixture_fingerprint for the simulated audiovisual-content signal.

Scenario universal_ad_id media_hash fixture_fingerprint Same underlying ad? Expected result
Exact same file Same or trusted Same Same Yes Resolve to 1 canonical creative
Same file, different source IDs Missing or different Same Same Yes Match on exact media hash
Same commercial, different transcodes Missing Different Same Yes Match on fixture fingerprint
Different commercials, same product Missing or different Different Different No Keep canonical creatives distinct
Different commercials, same advertiser Missing or different Different Different No Same advertiser does NOT mean same ad
Unknown creative identity Missing Different Missing Unknown Return unresolved/unknown

For example, two Toyota RAV4 candidates can share product_id: "rav4" while mapping to different canonical creative IDs. They must not be deduplicated just because they advertise the same product. Conversely, two source-local Toyota IDs with different transcodes could still be the same ad if their trusted universal ad IDs or fixture fingerprints match.

The 30-candidate fixture has 27 candidates with sufficient proof and three explicitly expected-unknown candidates. Its Toyota scenario is four candidates spanning two distinct RAV4 commercials: candidates 024/025 share one fingerprint and map to creative-003; 026/027 share another and map to creative-004. The two groups must remain distinct despite identical advertiser and product metadata.

Decision flow

  1. Load the 30 fixed ad offers and the trusted registry used to match source-specific ad IDs to the same underlying commercial.
  2. Use the seed to create a repeatable viewing history: households, devices, channels, ad breaks, channel changes, and playback results.
  3. Give every break the same 30 ad offers. The selector uses the trusted registry, but never uses the evaluation-only answer key.
  4. Replay the exact same history once with the baseline rules and once with the optimized rules. Each run starts with its own empty exposure store plus the same synthetic prior viewing history.
  5. For each break, preserve protected inventory, fill a 120-second pod, and temporarily reserve each selected ad.
  6. At the scheduled simulated time, record each selected ad as confirmed, canceled, delayed, or failed to settle.
  7. After both replays, use the separate answer key to measure repeated commercials and household frequency-cap violations.

For one illustrative household, the simulator may create:

  • household-001
  • household-001-device-01 watching channel-02
  • household-001-device-02 watching channel-05
  • A break on device 01 at simulated 00:12, followed by a break on device 02 at simulated 00:25
  • Four selected 30-second slots in each 120-second break
  • A source-local offer such as candidate-025 and another offer such as candidate-024 that the trusted registry identifies as the same canonical commercial

The exact devices, channels, and times depend on the seed, but the IDs and ordering are always reproducible.

CPM is a bid rate, not per-impression revenue. Estimated gross revenue for a confirmed impression is CPM / 1000.

Priorities

P0 β€” two-day MVP

Feature Implementation
Synthetic catalog Deterministic JSON candidates for multiple sources and canonical creatives
VAST fixtures Simulated VAST 3.0 inline responses; no live ad calls
Creative resolution Known Universal Ad ID, exact media hash, precomputed fingerprint, or unknown
Intra-pod deduplication Avoid exact-creative duplicates when eligible alternatives exist
Household caps Rolling-window state across simulated channels and devices
Atomic reservations map + sync.Mutex, reservation TTLs, confirmation, and cancellation
Pod selection Greedy CPM-aware selection subject to duration and inventory constraints
Reproducible simulator Fixed seed, immutable event stream, isolated strategy replays
Playback lifecycle Confirmed, canceled, delayed, and expired reservations
Metrics and tests Settlement, fill, revenue, duplicate, cap, identity, and determinism metrics

P1 β€” after the MVP

  • Playback-aware completed-break state for rewind/replay.
  • Product-level separation, language eligibility, and ad-safety metadata.
  • Decision explainability and a lightweight viewing timeline.
  • More complete VAST parsing, wrappers, tracking events, and policy validation.
  • Decision budgets and deterministic degraded-mode behavior.

P2 β€” scale and production-oriented exploration

  • Simulated UID2 and LiveRamp RampID integrations for consent-aware audience identity mapping and cross-device/household frequency-capping experiments.
  • Real media processing with FFmpeg, audio fingerprints, and perceptual video hashes.
  • VAST 4.x, OpenRTB pod bidding, and registry-backed creative-ID validation.
  • Redis-backed frequency storage for shared atomic state across multiple service instances.
  • Docker packaging, Kubernetes deployment, load testing, and fault injection.
  • Event pipelines and advanced pod optimization.

Repository layout

adpod-lab/
β”œβ”€β”€ README.md
β”œβ”€β”€ data/{candidates,creatives,identity_registry,ground_truth}.json
β”œβ”€β”€ go.mod
β”œβ”€β”€ cmd/simulator/main.go
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ creative/resolver.go
β”‚   β”œβ”€β”€ frequency/{store.go,memory.go}
β”‚   β”œβ”€β”€ model/
β”‚   β”œβ”€β”€ pod/selector.go
β”‚   └── simulator/{simulator.go,simulator_test.go}
β”œβ”€β”€ cmd/generate-fixtures/main.go
└── ..._test.go

There is no Dockerfile, Compose file, Redis client, or service dependency in the MVP path.

Run and verify

go test ./...
go test -race ./...
go run ./cmd/simulator \
  -seed 20261011 \
  -households 20 \
  -devices 3 \
  -channels 5 \
  -minutes 120

The default run creates 20 households, gives each household between one and three devices, chooses among five fictional channels, and simulates two hours. Use -households 100 to run the larger scenario. The seed controls the generated viewing behavior and playback outcomes; the committed 30 candidate fixtures remain the shared inventory for both strategies.

For example, if device 01 has already shown canonical commercial creative-003, the baseline only records that exposure under device 01 and the source-local ID it received. Device 02 can therefore select another source's offer for the same commercial. The optimized run translates both offers to creative-003, shares the exposure across the household, and can reject the second offer when the rolling frequency limit is reached. It also avoids putting two offers for the same recognized commercial in one pod.

The optimized run therefore adds two kinds of precision: better identity precision across ad sources, and better household-level exposure accounting. Those protections can reduce fill or revenue when no eligible alternative remains; the simulator reports that tradeoff instead of assuming an improvement.

Sample simulator output:

AdPod Lab Simulator
  Seed: 20261011  Households: 20  Devices: <=3  Channels: 5  Minutes: 120
  Candidates: 30  Events: 1944

Metric                                       baseline      optimized
Processed breaks                                  361            361
Ad opportunities                                 1444           1444
Selected ads                                     1444           1332
Confirmed impressions                            1222           1117
Canceled impressions                              151            146
Delayed confirmations                              62             58
Failed settlements (aggregate)                     71             69
Incomplete pods                                     0             28
Intra-pod duplicates                              226              0
Frequency-cap violations                          626              0
Estimated revenue                            $48.8221       $41.4609
Confirmed revenue                            $41.1716       $34.6367
Fill rate                                     100.00%         92.24%
Unresolved identities                               3              3

Output terms:

  • AdPod Lab Simulator: the local program that creates the viewing history, makes both ad decisions, and prints this comparison.
  • Seed: the starting number for the repeatable scenario generator. The same seed and flags produce the same households, events, outcomes, and report.
  • Households: the number of synthetic viewer homes created for the run.
  • Devices: <=3: each household receives one, two, or three simulated devices; three is the maximum set by the -devices flag.
  • Channels: the number of fictional channels available to those devices.
  • Minutes: the length of simulated viewing time. This is simulated time, so a 120-minute run completes quickly.
  • Candidates: the number of ad offers available to every break. The candidate pool contains 30 offers from multiple simulated sources.
  • Events: the generated viewing actions, ad-break requests, channel changes, and playback callbacks. They are created once and shared by both runs.
  • Metric: one measured result. The baseline and optimized columns show the same result for the two rule sets.
  • Processed breaks: the number of ad breaks that occurred in the generated viewing history.
  • Ad opportunities: the number of available ad slots. Each 120-second break has four 30-second slots, so 361 breaks produce 1,444 opportunities.
  • Selected ads: the number of offers placed into pods before playback is settled. A selected ad can later be canceled or fail to settle.
  • Confirmed impressions: selected ads whose playback was successfully confirmed.
  • Canceled impressions: selected ads canceled before confirmation because the simulated viewer left or changed channels.
  • Delayed confirmations: confirmations that arrived later than the normal playback callback but still arrived before the reservation expired.
  • Failed settlements (aggregate): selected ads whose later callback could not be applied, usually because the reservation expired or was unknown. The current store interface combines those causes.
  • Incomplete pods: breaks where the rules could not fill all four slots.
  • Intra-pod duplicates: repeated versions of the same underlying commercial within one pod. This is evaluated with the trusted ground-truth mapping, not only with the selector's prediction.
  • Avoidable / protected duplicates: the report separates duplicates that could have been avoided from duplicates caused by protected inventory.
  • Frequency-cap violations: confirmed impressions beyond the configured limit for one household and one commercial during the rolling time window. Each excess impression is counted once, not every possible pair.
  • Estimated revenue: the potential gross value of every selected ad, calculated as its CPM divided by 1,000, whether or not it is confirmed.
  • Confirmed revenue: the gross value of confirmed impressions only, using CPM divided by 1,000.
  • Fill rate: the percentage of available slots that received a selected ad.
  • Unresolved identities: candidate offers for which the trusted registry could not confidently identify the underlying commercial.

The baseline is the control experiment. It still safely reserves, confirms, and cancels ads, but it treats each device and each source's ad ID separately. It can therefore select two source offers that are actually the same commercial, and an exposure on device 01 does not affect device 02.

The optimized run uses the same lifecycle, but adds trusted cross-source identity matching, household-wide exposure history, rolling-window frequency caps, and avoidance of recognized duplicates within a pod. This makes the comparison fair: the optimized run is more precise about identity and household history, not simply better because the baseline was made unsafe.

The simulator reports actual tradeoffs; it does not hard-code an expected revenue or fill improvement.

MVP limitations

  • The in-memory store is correct only in one Go process and loses data on restart.
  • Fixture fingerprints model resolution proof; they are not extracted from media.
  • JSON fixtures are controlled input; VAST parsing and live integrations are intentionally out of scope.
  • A greedy optimizer is intentionally simple and may not find the global optimum.
  • Decision-latency percentiles are not part of the current compact report; the simulation prioritizes deterministic lifecycle and outcome metrics.

Future distributed deployment

The local store is an intentional MVP boundary, not a claim of multi-instance correctness. In a horizontally scaled service, independent process-local maps could each admit exposure capacity for the same household. A future RedisFrequencyStore can preserve the interface while using shared atomic reservation operations. Docker becomes useful then for reproducibly running the service and Redis locally; Kubernetes and load tests follow once multiple instances are in scope.

References

About

AdPod Lab: CTV ad decision engine that uses creative identity resolution, household frequency capping, + revenue-aware optimization to reduce repetitive ads across fragmented ad sources.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages