by Intelliverse X

Ad mediation engine

app/mediation/ is header bidding for kiosk screens: every unsold loop slot is auctioned across all attached demand networks, the highest normalized eCPM above the screen's floor wins, and house/ContentX content fills whatever is left. Direct-sold sponsorship campaigns are guaranteed inventory — they take their purchased slot share before any auction runs and cannot be outbid.

Real DOOH networks don't all allow the same participation (Vistar's policy: "We expect you to implement waterfall logic… We do not allow you to host a secondary auction"), so every adapter is attached in one of three participation modes.

Decision order per slot

  1. Direct-soldSTORE.active_campaigns_for_machine (active, in-flight, targeting this machine) fills its weighted share of the loop rotation first. No fan-out happens for these slots.
  2. Reservation (mode="reservation", Blindspot-style booked plays) — walked sequentially in tier order before the auction. A booked play is a commitment, not a bid: if the network returns one, it runs, full stop.
  3. Auction (mode="auction") — every enabled auction adapter's request_bid(SlotContext) runs concurrently, each under its own hard timeout. Highest eCPM ≥ floor wins.
  4. Waterfall tiers (mode="waterfall_tier", Vistar-compliant) — never price-compared against auction bids. Walked in tier order only when the auction produced no clearing winner, and only then is the network even called — so a waterfall lease is always played, never leased-and-dropped (an unplayed lease damages the network's spend-rate metric). The first fill wins at its own price, floor notwithstanding (the floor gates whether we call, never whether we honor the response).
  5. House — anything unsold always gets the house creative. A kiosk never shows an empty slot.

Adapter contract

class MyNetwork:
    async def request_bid(self, ctx: SlotContext) -> Bid | None: ...

from app import mediation
mediation.default_engine().registry.attach(
    "my_network", MyNetwork(),
    enabled=True, timeout_seconds=0.25, respects_floor=True, priority=None,
    mode="auction",          # auction | waterfall_tier | reservation
    tier=None,               # walk order for waterfall/reservation modes
    auto_approve=False,      # False → creatives queue for operator approval
)

SlotContext carries screen id, machine, OpenOOH venue type + category id, geo, resolution, slot duration, impression multiplier (est. impressions per play), loop size, and the advertised floor. Registry changes (attach / detach / enable) are safe at runtime: auctions snapshot the enabled set at start, so an in-flight auction completes untouched and the next one sees the new config.

The five price bases

Bid.price_model expresses the network's native pricing; the engine normalizes everything to USD eCPM before comparing:

Basis Network shape Normalization
cpm OpenRTB seatbid.bid.price (Place Exchange) as-is
per_spot_nanos Vistar media_cost: gross nano-dollars per spot nanos / 10⁹ / imps_per_spot × 1000 — Decimal-exact, no float drift on large values; the network-quoted Bid.impressions beats the screen multiplier
per_play Blindspot booked per-play rate price / ipp × 1000
contract_cpm Adomni-style negotiated fixed rate (no price in response) as-is, flagged estimated=True for monthly reconciliation
flat_hourly hourly rate for one loop slot plays/hr = 3600 / (slot_sec × loop_slots), ecpm = price / (plays × ipp) × 1000

Real network adapters (behind flags, all off by default)

app/mediation/networks/ implements the four integrate-behind-flag networks from the DOOH dossier. Each is unit-tested against documented example payloads with injected transports — no live vendor calls anywhere in the suite.

Network Mode Price basis PoP deadline Flip on with
Vistar Media waterfall_tier (tier 1) per_spot_nanos one-shot proof_of_play_url, 15 min KIOSKX_NET_VISTAR_ENABLED + KIOSKX_VISTAR_NETWORK_ID / KIOSKX_VISTAR_API_KEY / KIOSKX_VISTAR_API_URL
Place Exchange auction (OpenRTB 2.6: imp.qty, imp.dt, dooh object) cpm burl on play, 1 h retry budget KIOSKX_NET_PX_ENABLED + KIOSKX_PX_ENDPOINT / KIOSKX_PX_ORG_ID / KIOSKX_PX_TOKEN
Blindspot reservation (Pull API GET /api/sv/play/{deviceId}) per_play popUrl promptly, 15 min budget KIOSKX_NET_BLINDSPOT_ENABLED + KIOSKX_BLINDSPOT_BASE_URL / KIOSKX_BLINDSPOT_PER_PLAY_USD / KIOSKX_BLINDSPOT_DEVICE_MAP
Adomni auction (VAST pull) contract_cpm <Impression> URL, 4 h, t=<epoch-ms> param KIOSKX_NET_ADOMNI_ENABLED + KIOSKX_ADOMNI_DEVICE_KEY / KIOSKX_ADOMNI_CONTRACT_CPM / KIOSKX_ADOMNI_API_URL

The Vistar request payload lives in one place (app.mediation.networks.vistar.build_get_ad_payload) and is shared with the legacy single-SSP path in app/ssp.py — the two cannot drift.

Until credentials exist, KIOSKX_MEDIATION_MOCKS (default true) attaches the built-in mock set at startup — mock_px (auction, CPM), mock_adomni (auction, contract CPM, estimated), mock_vistar (waterfall tier 1, nano-dollar spots) — whose mock:// PoP callbacks settle in-process, so production exercises the full pipeline with zero external traffic.

Proof-of-play: deadlines, retries, expiry

report_play is one-shot per auction (Vistar semantics). External winners' receipts land in a PoP dispatch queue (app/mediation/pop.py):

The queue is in-memory (STORE architecture) but duck-typed — anything with enqueue(receipt, played_at=) can replace it (MediationEngine(pop_queue=…)) for a DB-backed outbox later. Inspect it at GET /api/v1/mediation/pop.

Creative approval queue

Programmatic creatives are pending by default: the first time a (source, creative URL) pair is seen from an adapter without auto_approve=True, it enters the approval queue and the engine skips it (audit status creative_pending) — in every mode, at any price. getAd can never serve an unapproved programmatic creative.

Operator surface (ads:read / ads:write scopes):

Behaviors fixed by fiat

Behavior Rule
Missing impression multiplier Falls back to KIOSKX_AD_IMPRESSIONS_PER_PLAY (default 0.7). A network-quoted Bid.impressions (Vistar) always wins over the screen multiplier.
Floor Inclusiveecpm == floor clears. Per-screen overrides (raise or lower) replace the global default. respects_floor=False registrations (preferred/PMP deals) bypass the open floor but still lose to higher clearing bids. Reservation and waterfall responses are exempt (committed plays).
Tie-break Highest eCPM → lowest adapter priority number (default = registration order) → lexicographic source id. Deterministic, no randomness. Waterfall/reservation walk order: tier (default = priority) → priority → source id.
Invalid bids Rejected, never clamped: price ≤ 0 (including 0/negative nano-dollars), normalized eCPM > KIOSKX_MEDIATION_MAX_ECPM (default $500), missing creative URL, creative longer than the slot. All logged, all recorded in the auction's audit trail.
eCPM spoofing The engine always recomputes eCPM from the native price; an adapter-supplied ecpm is ignored.
Fault isolation Adapter timeout / exception → no-bid for this auction (auction mode) or walk to the next tier (waterfall/reservation); the rest proceed. All adapters hanging → house within the max adapter timeout.
PoP One-shot per auction; repeats / unknown / stale ids return None, never an error. Direct and house wins produce internal PoP only and never enter the dispatch queue.

/apk/getAd wiring

Set KIOSKX_MEDIATION_ENABLED=true and getAd's programmatic stage (step 3, after direct campaigns and operator ads) fills through the engine via mediation.request_fill(machine, unsold_slots) instead of the single-SSP adapter. Winning slots register mediation leases so the firmware's existing /apk/saveAdRecord report settles PoP with the winning network through the dispatch queue. Flag off (default) keeps the legacy Vistar-style single-SSP path — behavior is byte-identical to before this module existed.

Standalone use (no getAd): await mediation.select_slot(screen_ctx) decides one slot; build screen_ctx with mediation.slot_context_for(machine).

Config

Env Default Meaning
KIOSKX_MEDIATION_ENABLED false Wire the engine into /apk/getAd's programmatic stage.
KIOSKX_MEDIATION_MOCKS true Attach the built-in mock demand set at startup.
KIOSKX_MEDIATION_FLOOR_ECPM 0.0 Global open-auction floor (USD eCPM).
KIOSKX_MEDIATION_MAX_ECPM 500.0 Reject-above cap for absurd bids.
KIOSKX_NET_TIMEOUT_SECONDS 3.0 Per-adapter timeout for real networks.
KIOSKX_NET_{VISTAR,PX,BLINDSPOT,ADOMNI}_ENABLED false Real network flags (see table above for their credential vars).

Verification