Ad Ops Dashboard Guide

Internal playbook for operating the Bidding Player platform — how to use the dashboard, how to onboard new publishers, and how to extend the system with new SSPs.

v2.5.0 Multi-bidder Internal docs

What this is

Bidding Player is a one-script video header-bidding solution we ship to publishers. Each publisher pastes our <script> tag into their page; the engine runs a Prebid auction, stitches the winning bid into their GAM master ad tag, and plays the resulting VAST through video.js + IMA.

The ad ops team owns three things on this platform:

  1. Tag generation — using the dashboard to produce a publisher-specific <script> snippet.
  2. SSP onboarding — triggering a new Prebid bundle build when a publisher needs a new bidder.
  3. Operations — testing tags before handoff, debugging publisher-reported issues, and managing version rollouts.

Architecture & pieces

Three artifacts ship to each publisher. All three are hosted on jsDelivr at versioned, immutable URLs:

ArtifactWhat it doesPer-publisher?
engine/player.js The shared player engine. Reads data-* attributes, registers a video ad unit, runs the auction, stitches hb_* targeting into the GAM tag, plays the creative via IMA. One file
prebid/<bundle>.js The Prebid.js library, custom-built with only the bidder adapters a specific publisher needs (smaller payload, faster load). Catalog
<script> snippet The HTML tag pasted into the publisher's page. Carries the publisher's bidder params, GAM tag URL, content URL, player config. Unique
Key idea

One engine + N bundles + M snippets. The engine never changes per publisher. The bundle is shared by all publishers using the same set of SSPs. The snippet is unique per placement.

Sandbox testing

The Sandbox Testing Suite tab has two modes, set by the Bid Source selector in Auction Configuration:

  • Simulated (default) — inject a mock CPM and verify the full GAM stitch + IMA playback path end-to-end. Does not call real bidders.
  • Real bid request — fires a live pbjs.requestBids() against the ticked SSPs and reports each bidder's real status (bid / no-bid / timeout / error), CPM, and response time. Use it to QA adapter wiring + params and measure SSP latency. Caveat: requests come from the dashboard's domain, which is usually not allowlisted, so no-bid is expected here — run from an allowlisted publisher page to see live CPMs. The timings, adapter firing, and errors are real regardless.

When to use it

  • Before handing a tag to a publisher — verify the GAM tag responds and IMA plays the creative.
  • When debugging "ad not playing" reports — the structured phase logs (Phases 1–9) show exactly where the chain breaks.
  • To check that a new Prebid bundle initializes and registers without errors.

How to read the log

The log is divided into 9 numbered phases. Skim phase headings first; expand only the phase that errored. Phase 6 ("Appending Prebid response into GAM master ad tag") is where the stitched URL appears — confirm hb_pb, hb_bidder, and hb_uuid all show up in cust_params.

Tag generator

The Production Tag Generator tab produces the deployable <script> snippet for a publisher. The flow:

  1. Fill the GAM master ad tag

    Paste the publisher's GAM master tag URL. It must end with correlator= so the engine can append a unique correlator per impression. If cust_params is missing, the engine adds it.

  2. Choose the placement

    Set Placement to instream (ad plays against a content video — needs a Video Content URL) or outstream (in-article, no content video; the slot autoplays muted on scroll-into-view and collapses when done). Outstream dims the Video Content URL field automatically.

  3. Set pricing controls (optional)

    Strategic Bias (off by default), Floor Min, and Floor Max are toggles in Ad Serving — flip them on only when the publisher needs them. For instream, the Sticky / Floating Player toggle (in Player Behaviour) makes the player float to the corner on scroll-out.

  4. Tick the bidders

    In the Bidders card, tick every SSP that should compete in this auction. Limelight is on by default. For each ticked bidder, fill in its required params (e.g. publisher ID, placement ID).

  5. Verify the Prebid URL matches

    The engine + Prebid URLs are pinned to the latest release automatically (the version selector was removed in v2.3.0). They live in the collapsible Hosting & CDN panel — override only when testing a feature branch. The Prebid URL must point at a bundle that contains every adapter you ticked — see Prebid bundle catalog.

  6. Generate & copy

    Click Generate Production Tag. Validation runs first — required fields and bidder params get red borders if invalid. Once green, copy the snippet from the output box and hand it to the publisher.

Validation gotcha

Numeric bidder params (Ad Unit ID, Placement ID, Account ID, etc.) accept text but must parse as numbers. If a publisher gives you a value like "972556929-A", strip the suffix or push back — Prebid will reject the request.

Field reference

FieldPurposeRequired
GAM Master Ad Tag URLPublisher's GAM tag, must end with correlator=.Yes
Placementinstream (ad against content video) or outstream (in-article, no content video; autoplays muted on scroll-into-view, collapses when done). Emits data-placement.Yes
Video Content URLThe video the publisher wants in the player; pre-roll plays before it. Instream only — dimmed for outstream.Instream
Bidder TimeoutHow long Prebid waits for bid responses before calling the auction. Default 1200 ms.No
Strategic BiasConstant added to the winning bid's hb_pb before stitching, to tip ties to header bidding. Toggle — off by default; off emits data-bias="0.00".No
Floor Min / Floor MaxToggles. Min rejects sub-floor bids (fall through to house); Max caps the winning CPM before bias + bucketing. Emit data-floor-min / data-floor-max when on.No
Prebid.js URLThe bundle file in prebid/ that contains the adapters you ticked. Pinned to the latest release; in the Hosting & CDN panel.Yes
Bidder pickerOne card per SSP. Tick to include; fill params. Generates the data-bidders JSON.≥1
Sticky / Floating PlayerInstream only. Floats the player to the corner on scroll-out (keeps playing, × to close). Emits data-sticky="true". Off by default.No
Loop ContentReplays the content video after it ends. On by default — keeps inventory available for refresh.No
VPAID Access Modeinsecure = highest yield (allows insecure VPAIDs). Switch to enabled only if the publisher requests it.No

Prebid bundle catalog

Prebid.js is modular — every bidder is an adapter you opt into at build time. Shipping every adapter would bloat the file (3 MB+ for the full Prebid catalog). Instead, we maintain a catalog of pre-built bundles, each containing just the adapters a particular set of publishers needs.

Catalog layout

Bundles live under prebid/ in the repo. Naming convention for custom bundles: prebid-<bidder1>-<bidder2>.js. The current catalog:

FileAdaptersSizeUsed by
prebid/prebid.js
Default
limelightDigital, appnexus, rubicon (Magnite), pubmatic, openx, incrementx 286 KB Every publisher — covers the 6 common SSPs out of the box

Every bundle automatically includes the consent modules (consentManagementTcf, consentManagementUsp, consentManagementGpp, tcfControl) — the engine's default Prebid config depends on them.

v2.1.3 change

The default prebid.js now ships with all 5 SSPs (Limelight + AppNexus + Magnite + PubMatic + OpenX). No more "build a new bundle for each publisher" workflow for the common case. Only build a custom bundle when a publisher needs an SSP outside this set.

Triggering a new bundle build

When a publisher needs an SSP we don't have a bundle for yet, build a new one. This is a GitHub Actions workflow — no local tooling needed.

  1. Open the Actions tab

    Go to github.com/shashwatsilverpush/bidding-player/actions. Select Build Prebid bundle from the workflow list on the left.

  2. Click "Run workflow"

    Top-right of the runs list. A form drops down with three inputs.

  3. Fill the inputs

    modules — comma-separated Prebid adapter names. Find names at docs.prebid.org/dev-docs/bidders.html. Example: limelightDigitalBidAdapter,appnexusBidAdapter.

    bundle_name — output filename without .js. Follow the convention: prebid-limelight-appnexus.

    prebid_version — Prebid.js release tag, e.g. v11.11.0. Stick to the same version we've been building against unless there's a reason to upgrade.

  4. Run & wait

    ~3 minutes. The workflow clones Prebid.js, runs gulp build --modules=…, commits the result to prebid/<bundle_name>.js on main.

  5. Tag a release

    Once the commit lands, bump VERSION and tag the repo: git tag v2.X.Y && git push --tags. jsDelivr will serve the new bundle at @v2.X.Y within a few minutes.

  6. Use it in the tag generator

    In the dashboard, change the Prebid URL filename (in the Hosting & CDN panel) to your new bundle, tick the new bidder, fill its params, generate the tag. The engine/Prebid URLs are pinned to the latest release automatically.

If a build fails

Open the failed run, expand "Build bundle" step. The most common error is a typo in the module name — Prebid will list the unknown module. Fix and re-run.

Adding a bidder to the picker UI

The bidder picker in the dashboard reads from a single JS array in index.html: BIDDER_CATALOG. To make a new SSP selectable in the UI, add an entry there.

// index.html, inside adopsModule()
const BIDDER_CATALOG = [
  { id: 'limelightDigital', label: 'Limelight Digital', module: 'limelightDigitalBidAdapter', defaultOn: true, params: [...] },
  { id: 'appnexus',        label: 'AppNexus / Xandr',   module: 'appnexusBidAdapter',        params: [
    { key: 'placementId', label: 'Placement ID', required: true, type: 'number' }
  ]},
  // ← add a new bidder here
];

Fields per entry:

  • id — the Prebid bidder code (used as the "bidder" key in the request).
  • label — what shows in the UI.
  • module — the adapter name (only used as a hint label in the UI; the actual module is included via the Prebid bundle).
  • defaultOn — if true, the checkbox is ticked on first render.
  • params — array of param schemas: key, label, required, type ('number' or omit for string), default, hidden (auto-injected without a UI input).

Adding here does not automatically include the adapter in any bundle — the bundle build must include the matching module name. Order is: build bundle → add to catalog → use in tag.

Publisher onboarding playbook

What to ask the publisher

  1. Their GAM master ad tag URL (must end with correlator=).
  2. Which SSPs they want competing (and the bidder-specific IDs — publisher ID, placement ID, etc.).
  3. The video content URL.
  4. Their domain — required to allowlist with the SSPs.

Pre-launch checklist

  • Domain is allowlisted at every SSP they're using. #1 cause of zero fill.
  • GAM line items targeting hb_pb are set up at $0.10 increments.
  • VAST wrapper creative uses %%PATTERN:hb_uuid%%prebid.adnxs.com/pbc/v1/cache.
  • Tag tested locally with ?debug=true — auction runs, GAM responds, IMA plays creative.

Handoff

Send the publisher: the generated <script> snippet, a link to the publisher integration guide, and the PDF version for offline reference. Their docs do not reference this ad ops console — keep internal flow internal.

Release & deploy cycle

  1. Land changes on main. Bump VERSION file (the CI version-drift check enforces consistency between VERSION and embedded jsDelivr URLs).
  2. Tag the release: git tag v2.X.Y && git push --tags.
  3. Update the dashboard's default version field if appropriate (in index.html, the adopsVersion input).
  4. Promote to the auto-update channel: set stable in engine/channel.json to the new version, commit, then purge https://purge.jsdelivr.net/gh/shashwatsilverpush/bidding-player@main/engine/channel.json (and @2/engine/loader.js) so auto-update tags pick it up within minutes.
  5. Rollback (if a release misbehaves): set channel.json stable back to the previous version, commit, purge — every auto-update publisher reverts. Canary: set canary + canaryPct (0–100) to roll a release to a sticky % of viewers first.
  6. Pinned publishers on older tags are unaffected — jsDelivr serves immutable bytes per tag.
PDF auto-regen

When publisher-guide.html changes on main, the pdf-regen.yml workflow regenerates publisher-guide.pdf via Chrome headless. No manual step needed.

Common issues

Publisher reports "no ads playing"

  1. Open their page with ?debug=true. Check the engine log.
  2. Phase 3 (auction) — did Prebid issue a bid request? If not, the bundle is missing the adapter.
  3. Phase 4 (response) — empty seatbid? Domain allowlisting issue at the SSP.
  4. Phase 6 (stitch) — is hb_uuid in the GAM URL? If missing, engine version is < v1.2.0; ask publisher to update.
  5. Phase 8 (IMA) — VAST loads but doesn't play? Usually VPAID mode mismatch or mixed content.

"Auction canceled" in the log

Consent module canceled it. Check the consent config — we ship with static TCF (gdprApplies: false) and dropped GPP defaults in v1.2.1/1.2.2 specifically to prevent this. If the publisher has a real CMP, switch the engine config from static to iab.

Bidder silently dropped

Prebid logs a warning like "video ad unit containing bidders that don't support video: X". The bundle they're loading doesn't include the adapter. Either rebuild the bundle including the missing adapter, or remove that bidder from the snippet.

Tag generator validation never passes

Open browser DevTools → Console. Look for red errors. The most common cause is a paste of a GAM URL with smart quotes from a Google Doc — re-paste from a plain-text source.

Changelog

  • Dashboard
    Sandbox real bid request mode. A "Bid Source" selector in the Sandbox: Simulated (mock CPM, default) or Real bid request — fires a live pbjs.requestBids() and shows each SSP's real status, CPM, and response time. Dashboard-only (no engine change). Expect no-bid from the dashboard origin; run from an allowlisted page for live CPMs.
  • v2.5.0
    Auto-updating engine tag. New "Engine Delivery" choice in the tag generator: Auto-update (default) emits a tiny loader that loads the latest stable engine from a release channel (engine/channel.json), so publishers never re-issue the tag when we ship an update; Pinned keeps the old frozen @vX.Y.Z behavior. Rollback = set channel.json stable back + purge; canary = canary + canaryPct.
  • v2.4.3
    Outstream unmute control. Outstream ads autoplay muted, so the player now shows a tap-for-sound mute/unmute toggle (bottom-left) while the ad plays. IMA's own controls (ad label, countdown, clickthrough, skip) are unchanged — the toggle is an overlay for the one control IMA doesn't provide generically.
  • v2.4.2
    Outstream muted-autoplay fix. The outstream renderer never actually muted the ad, so the browser blocked its autoplay (outstream starts on scroll, with no user click) — the slot would expand to a black frame for a split second and then collapse. The engine now mutes the outstream ad (video element + adsManager.setVolume(0)) so muted autoplay is allowed, and logs IMA ad errors under ?debug=true.
  • v2.4.1
    Outstream loop fix. data-loop is a content-video setting and is now ignored for outstream in the engine, and omitted from generated outstream tags by the dashboard. Fixes a report where an outstream tag carrying data-loop="true" made the ad flash for a split second and then disappear (the loop attribute restarted the ad creative in the shared video element, colliding with the ALL_ADS_COMPLETED collapse).
  • v2.4.0
    Sticky / floating instream player. New data-sticky="true" attribute (and a "Sticky / Floating Player" toggle in Player Behaviour, off by default). When an instream player scrolls out of view it floats to the bottom-right corner and keeps playing — with a × to dismiss — then snaps back inline on scroll-up. The original layout space is preserved and the live IMA ad creative is resized to fit the floating frame. Instream only (outstream already auto-collapses). Lifts viewability + completed-view rates.
  • v2.3.0
    Outstream video support. New data-placement="outstream" and a Placement dropdown in the tag generator. Outstream needs no content video: the slot stays collapsed until it scrolls ≥50% into view, autoplays muted, then collapses on completion/error. All engine render paths now route through a render() dispatcher by placement. Also: the dashboard's Release Version selector was removed — engine/Prebid URLs are pinned to the latest release (override in Hosting & CDN).
  • v2.2.0
    Floor price controls + GAM key-value reference. New data-floor-min (reject sub-floor bids → house line item) and data-floor-max (cap the winning CPM before bias + bucketing), wired into the engine, tag generator (toggles), and sandbox. Added a GAM Key-Value reference tab documenting standard + per-bidder hb_* keys, the %%PATTERN:hb_uuid%% macro, and line-item setup.
  • v2.1.3
    Default bundle expansion + Pages deploy fix. The default prebid/prebid.js now contains all 5 common SSPs (Limelight + AppNexus + Magnite + PubMatic + OpenX) built from Prebid.js 11.13.0 — eliminates the "build a new bundle per publisher" step for the common case (286 KB total, +37 KB over the v2.0.0 Limelight-only build). Migrated GitHub Pages deploy from legacy Jekyll builder to actions/deploy-pages workflow after the legacy builder hung twice on large content commits. Fixed a latent bug in build-prebid-bundle.yml — Prebid.js tags lack the v prefix (e.g. 11.13.0, not v11.13.0). Added .nojekyll.
  • v2.0.0
    Multi-bidder support. New data-bidders JSON attribute lets a single tag run an auction across multiple SSPs. Per-publisher Prebid bundle catalog under prebid/. workflow_dispatch CI for on-demand bundle builds. Dashboard now has a checkbox-based bidder picker (replaces v1's JSON textarea). Legacy data-pub-id / data-adunit-id / data-host attrs continue to work for v1 tags in the wild.
  • v1.2.2
    TCF static config. Switched to cmpApi: 'static' with gdprApplies: false so tcfControl stops canceling auctions when no CMP is present on the publisher page.
  • v1.2.1
    Dropped GPP from defaults. The GPP consent module was canceling auctions on every publisher without a GPP-aware CMP. Pulled it from the default config.
  • v1.2.0
    Full hb_* targeting. Engine now stitches the complete targeting set (hb_uuid, hb_size, hb_format, hb_adid) into GAM's cust_params, not just hb_pb/hb_bidder. Without hb_uuid, GAM couldn't resolve cached VAST creatives. Also: dynamic player size (reads getBoundingClientRect at auction time); consent management modules wired in.
  • v1.1.0
    Custom Limelight Prebid build. The stock Prebid bundle was silently dropping the Limelight adapter ("video ad unit containing bidders that don't support video: limelightDigital"). Swapped in a build with the supplier-provided adapter.
  • v1.0.x
    Initial release. Versioned jsDelivr hosting, CI version-drift guard, sandbox testing tab, production tag generator, structured phased logging, Claude high-contrast dark theme, publisher PDF guide with auto-regen workflow.