Auction Orchestration
Learn how Trusted Server coordinates multiple demand sources in parallel to maximize revenue and minimize latency.
Overview
The auction orchestrator is the core system that manages server-side ad auctions. It launches bid requests to multiple demand providers simultaneously, collects responses, and selects winners.
Key capabilities:
- Parallel execution — Bid requests to all providers launch concurrently using Fastly's
select()API - Strategy-based winner selection — Automatic strategy detection based on configuration
- Mediator support — Optional external mediator for final winner selection and unified floor pricing
- Provider abstraction — Pluggable provider interface for adding new demand sources
- Creative processing — Winning creatives are rewritten to first-party proxy URLs by default, with opt-in sanitization
System Flow (Prebid + APS)
The following diagram shows the full auction flow when both Prebid and APS providers are configured with a mediator:
Architecture
Request Flow
The auction system processes requests through a pipeline of transformations:
POST /auction (AdRequest in Prebid.js format)
│
├─ Parse body → AdRequest { adUnits[] }
├─ Generate EC + fresh user IDs
├─ Convert adUnits → AdSlots with formats and bidder params
├─ Extract device info (User-Agent, geo)
│
▼
AuctionOrchestrator.run_auction()
│
├─ Detect strategy (parallel_only or parallel_mediation)
├─ Launch all providers in parallel via select()
├─ Collect responses as they complete
│
├─[parallel_only]─── Select highest decoded CPM per slot
└─[parallel_mediation]─── Forward decoded-price bids to mediator for final selection
│
▼
Convert OrchestrationResult → OpenRTB 2.x Response
│
├─[sanitize_creatives=true] Strip executable markup
├─[rewrite_creatives=true] Rewrite URLs and inject creative TSJS
├─ Add ext.orchestrator metadata
└─ Set consent and optional EID response headersKey Components
The orchestrator is composed of several modules:
| Module | Path | Purpose |
|---|---|---|
orchestrator.rs | crates/trusted-server-core/src/auction/ | Core parallel execution and bid selection |
provider.rs | crates/trusted-server-core/src/auction/ | AuctionProvider trait definition |
types.rs | crates/trusted-server-core/src/auction/ | Data structures (AuctionRequest, Bid, etc.) |
formats.rs | crates/trusted-server-core/src/auction/ | Format conversions (TSJS ↔ OpenRTB) |
endpoints.rs | crates/trusted-server-core/src/auction/ | HTTP handler for POST /auction |
config.rs | crates/trusted-server-core/src/auction/ | Auction configuration types |
Provider Auto-Discovery
Providers register themselves at startup via builder functions. The build_orchestrator() function in auction/mod.rs iterates all registered builders, passes the application settings, and each builder returns zero or more providers depending on whether its config section is present and enabled:
// Each integration registers its own builder
fn provider_builders() -> &'static [ProviderBuilder] {
&[
prebid::register_auction_provider,
aps::register_providers,
adserver_mock::register_providers,
]
}This means you only need to add a config section to trusted-server.toml for a provider to be automatically discovered and registered.
Auction Strategies
The orchestrator automatically selects a strategy based on whether a mediator is configured.
Parallel Only
When no mediator is set, the orchestrator runs all providers in parallel and selects winners by comparing decoded prices directly. This is the simplest strategy.
[auction]
enabled = true
providers = ["prebid", "aps"]
# No mediator — direct price comparison
timeout_ms = 2000How winner selection works:
- Collect bids from all providers.
- Group bids by slot ID.
- Skip bids without a decoded numeric price.
- Select the highest CPM for each slot.
- Apply floor prices and drop winners below the slot's floor.
APS OpenRTB supplies decoded prices, so eligible APS bids participate directly without requiring a mediator.
Parallel Mediation
When a mediator is configured, provider responses are forwarded to the mediator service for final winner selection and unified floor pricing.
[auction]
enabled = true
providers = ["prebid", "aps"]
mediator = "adserver_mock" # Enables mediation
timeout_ms = 2000How mediation works:
- Run all providers in parallel (same as parallel_only).
- Collect all responses.
- Forward bids with decoded numeric prices to the mediator.
- Let the mediator apply policy and choose a winner.
- Restore render/accounting state from the selected source bid.
- Filter any mediator winner without a decoded price.
Mediation is optional for APS. APS reduces to one candidate per impression before mediation so the selected renderer can be restored without same-slot ambiguity.
Providers
Provider Interface
All demand sources implement the AuctionProvider trait:
pub trait AuctionProvider: Send + Sync {
fn provider_name(&self) -> &'static str;
fn request_bids(
&self,
request: &AuctionRequest,
context: &AuctionContext<'_>,
) -> Result<PendingRequest, Report<TrustedServerError>>;
fn parse_response(
&self,
response: fastly::Response,
response_time_ms: u64,
) -> Result<AuctionResponse, Report<TrustedServerError>>;
fn supports_media_type(&self, media_type: &MediaType) -> bool;
fn timeout_ms(&self) -> u32;
fn is_enabled(&self) -> bool;
fn backend_name(&self) -> Option<String>;
}The trait uses a two-phase design:
request_bids()— Builds and sends the HTTP request, returning aPendingRequest(Fastly's async handle)parse_response()— Called once the response arrives, parses the provider-specific format into a unifiedAuctionResponse
This split enables true parallel execution: all requests launch first, then the orchestrator uses select() to process responses as they arrive.
Prebid Provider
Transforms auction requests into OpenRTB 2.x format and sends them to a Prebid Server instance.
Request transformation:
AdSlot→ImpwithBanner { format: [Format { w, h }] }- Bidder params from slot config →
ext.prebid.biddermap - EC and fresh user IDs injected into
Userobject - Device info, geo data, and GPC signals included
- Optional Ed25519 request signing (see Request Signing)
Response parsing:
- Bids include decoded
price(clear decimal CPM) - Creative HTML provided in
admfield - Winning creative URLs rewritten to first-party proxy format by default when the
/auctionresponse is assembled - Per-bidder timing (
responsetimemillis), errors, and warnings always attached as response metadata - When
debugis enabled, PBS debug payload and per-bid status (bidstatus) also included
[integrations.prebid]
enabled = true
server_url = "https://prebid-server.example.com"
timeout_ms = 1000
bidders = ["appnexus", "rubicon"]APS Provider
Builds an independent banner OpenRTB request for Amazon Publisher Services.
Request transformation:
- banner
AdSlotformats become secure OpenRTB impressions; ext.accountuses canonicalaccount_id;ext.sdkidentifies the compatible Prebid contract; and- existing page, device, consent, identity, and geo privacy gates are preserved.
Response parsing:
- decoded USD prices compete directly with other providers;
- positive compatible dimensions and an HTTPS
creativeurlare required; - script creatives are rejected before winner selection unless explicitly enabled;
- one candidate per impression is retained deterministically; and
- a minimized typed renderer is preserved instead of creative markup or APS notifications.
[integrations.aps]
enabled = true
account_id = "example-account"
timeout_ms = 800
debug = false
allow_script_creatives = falseSee APS OpenRTB Integration for rollout and rendering requirements.
AdServer Mock Mediator
An external mediation service that receives decoded-price bidder responses and performs final winner selection. APS prices are already decoded at the provider boundary.
Mediation request format:
{
"id": "auction-123",
"imp": [
{ "id": "header-banner", "banner": { "format": [{ "w": 728, "h": 90 }] } }
],
"ext": {
"bidder_responses": [
{
"bidder": "aps",
"bids": [{ "imp_id": "header-banner", "price": 2.5, "adm": null }]
},
{
"bidder": "prebid",
"bids": [
{ "imp_id": "header-banner", "price": 2.0, "adm": "<html>..." }
]
}
],
"config": { "price_floor": 0.5 }
}
}Mediation response: Standard OpenRTB with decoded prices and selected winners.
[integrations.adserver_mock]
enabled = true
endpoint = "https://your-mediator.example.com/adserver/mediate"
timeout_ms = 500
price_floor = 0.50Data Structures
AuctionRequest
The internal representation of an auction, converted from the incoming AdRequest:
pub struct AuctionRequest {
pub id: String, // UUID
pub slots: Vec<AdSlot>, // Ad placements
pub publisher: PublisherInfo, // Domain, page URL
pub user: UserInfo, // EC ID, fresh ID, consent
pub device: Option<DeviceInfo>, // UA, IP, geo
pub site: Option<SiteInfo>, // Domain, page
pub context: HashMap<String, serde_json::Value>, // Additional metadata
}AdSlot
Represents a single ad placement on the page:
pub struct AdSlot {
pub id: String,
pub formats: Vec<AdFormat>, // Supported sizes
pub floor_price: Option<f64>, // Minimum CPM
pub targeting: HashMap<String, serde_json::Value>, // Key-value targeting
pub bidders: HashMap<String, serde_json::Value>, // Per-bidder params
}Bid
The unified bid format used across all providers:
pub struct Bid {
pub slot_id: String,
pub price: Option<f64>, // Missing prices fail closed
pub currency: String,
pub creative: Option<String>, // APS uses renderer instead of markup
pub adomain: Option<Vec<String>>,
pub bidder: String,
pub width: u32,
pub height: u32,
pub nurl: Option<String>, // Win notification URL
pub burl: Option<String>, // Billing URL
pub renderer: Option<BidRenderer>,
pub metadata: HashMap<String, serde_json::Value>,
}The price field remains optional so missing-price bids fail closed. APS supplies a decoded price and a typed renderer instead of creative HTML; the renderer is retained through direct winner selection and mediation.
OrchestrationResult
The complete result of an auction:
pub struct OrchestrationResult {
pub provider_responses: Vec<AuctionResponse>, // All provider results
pub mediator_response: Option<AuctionResponse>, // Mediator result (if used)
pub winning_bids: HashMap<String, Bid>, // Slot ID → winning bid
pub total_time_ms: u64,
pub metadata: HashMap<String, serde_json::Value>,
}Input and Output Formats
Request Format (TSJS / Prebid.js)
The POST /auction endpoint accepts a Prebid.js-compatible AdRequest:
{
"adUnits": [
{
"code": "header-banner",
"mediaTypes": {
"banner": {
"sizes": [
[728, 90],
[970, 250]
]
}
},
"bids": [
{
"bidder": "appnexus",
"params": { "placementId": 12345 }
}
]
}
]
}Response Format (OpenRTB 2.x)
Auction results are returned in standard OpenRTB format with an ext.orchestrator metadata block:
{
"id": "auction-abc123",
"seatbid": [
{
"seat": "prebid",
"bid": [
{
"id": "bid-1",
"impid": "header-banner",
"price": 2.5,
"adm": "<iframe src=\"/first-party/proxy?tsurl=...&tstoken=sig\">...</iframe>",
"w": 728,
"h": 90
}
]
}
],
"ext": {
"orchestrator": {
"strategy": "parallel_mediation",
"providers": 2,
"total_bids": 3,
"time_ms": 145
}
}
}APS renderer winners use the same OpenRTB response with a typed renderer extension instead of adm:
{
"id": "auction-abc123",
"seatbid": [
{
"seat": "aps",
"bid": [
{
"id": "upstream-aps-bid-id",
"impid": "header-banner",
"price": 2.5,
"w": 728,
"h": 90,
"ext": {
"trusted_server": {
"renderer": {
"type": "aps",
"version": 1,
"accountId": "example-account",
"bidId": "upstream-aps-bid-id",
"tagType": "iframe",
"creativeUrl": "https://creative.example/render",
"aaxResponse": "fictional-base64-envelope",
"width": 728,
"height": 90
}
}
}
}
]
}
]
}For these bids, id preserves APS's upstream bid ID, crid is present only when APS supplies one, and adm is absent. TSJS understands this contract; other /auction consumers must render ext.trusted_server.renderer explicitly.
EC identity is maintained with the ts-ec cookie; auction responses do not emit EC ID headers.
Creative Processing
Winning creatives returned by POST /auction pass through two independent transforms. sanitize_creatives (opt-in, default false) strips executable markup with its inner content. rewrite_creatives (default true) runs an HTML rewriter (lol_html) that converts eligible external resource and click URLs to signed first-party paths, adds data-tsclick, rewrites inline CSS url(...) values, removes bidder-supplied <base> elements, and injects the unified creative TSJS runtime exactly once, whether or not the bidder supplied a <body> element. In every mode, a creative larger than the 1 MiB per-creative cap is rejected and its adm is dropped.
[auction]
sanitize_creatives = false
rewrite_creatives = truesanitize_creatives | rewrite_creatives | Winning-bid adm behavior |
|---|---|---|
false (default) | false | Deliver the creative exactly as the bidder returned it (subject to the size cap). |
true | false | Strip executable markup, then deliver without rewriting. Accepted asset and click URLs remain direct. |
false | true (default) | Rewrite eligible URLs, add click-guard attributes, and inject creative TSJS into the raw bidder markup. Executable markup is preserved. |
true | true | Sanitize first, then rewrite eligible URLs, add click-guard attributes, and inject creative TSJS. |
When sanitization is enabled, scripts, stylesheets, style blocks, forms, event handlers, dangerous URL schemes, and other rejected content are removed together with their inner content — which blanks script-based creatives. Disabling rewriting removes the injected creative runtime and first-party proxy/click mediation from the resulting adm, so the browser may contact third-party hosts without mediation. Sanitizer-accepted hosts are not allowlisted or trusted merely because their URLs remain in the output.
Both settings apply to winning-bid adm in both the shared POST /auction response converter and the production publisher SSAT/page-bids path. The former emits root-relative first-party URLs and injects creative TSJS; the latter emits absolute first-party URLs for its foreign-origin renderer and does not inject that bundle. HTML/CSS returned by /first-party/proxy continues to be rewritten independently. [debug].inject_adm_for_testing adds the diagnostic debug_bid blob and enables a testing-only direct GAM replacement; it does not control whether processed adm is delivered.
Elements handled by the rewrite pass:
| Element | Attributes | Target |
|---|---|---|
<img> | src, data-src, srcset | /first-party/proxy?tsurl=... |
<script> | src | /first-party/proxy?tsurl=... |
<link> | href, imagesrcset | /first-party/proxy?tsurl=... |
<iframe> | src | /first-party/proxy?tsurl=... |
<video>, <audio>, <source> | src | /first-party/proxy?tsurl=... |
<a>, <area> | href | /first-party/click?tsurl=... |
<style>, [style] | url() references | /first-party/proxy?tsurl=... |
SVG <image>, <use> | href, xlink:href | /first-party/proxy?tsurl=... |
The rewrite pass leaves relative URLs and non-network schemes unchanged. When sanitize_creatives is also enabled, sanitization runs first and strips dangerous schemes, so only sanitizer-accepted values reach this pass; with sanitization disabled, the rewriter operates on the raw bidder markup. Domains in the rewrite.exclude_domains config list (supports wildcards like *.cdn.example.com) are also skipped.
Each proxied URL includes a tstoken HMAC signature for tamper protection. See Proxy Signing for details.
Configuration
Full Example
[auction]
enabled = true
sanitize_creatives = false # Opt-in; blanks script-based creatives when enabled
rewrite_creatives = true
providers = ["prebid", "aps"]
mediator = "adserver_mock" # Remove for parallel_only strategy
timeout_ms = 2000
[integrations.prebid]
enabled = true
server_url = "https://prebid-server.example.com"
timeout_ms = 1000
bidders = ["appnexus", "rubicon"]
auto_configure = true
debug = false
[integrations.aps]
enabled = true
account_id = "example-account"
timeout_ms = 800
debug = false
allow_script_creatives = false
[integrations.adserver_mock]
enabled = true
endpoint = "https://your-mediator.example.com/adserver/mediate"
timeout_ms = 500
price_floor = 0.50Configuration Reference
[auction]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable the auction system |
sanitize_creatives | bool | false | Strip executable markup from winning-bid adm before delivery |
rewrite_creatives | bool | true | Rewrite winning-bid adm through first-party endpoints |
providers | string[] | [] | Ordered list of provider names to call |
mediator | string? | null | Provider name to use as mediator (enables parallel_mediation) |
timeout_ms | u32 | 2000 | Overall auction timeout in milliseconds |
Both creative-processing fields must be present in the TOML for their environment overrides to apply; see Environment Variable Overrides.
[integrations.prebid]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable Prebid provider |
server_url | string | — | Prebid Server URL (required) |
timeout_ms | u32 | 1000 | Request timeout |
bidders | string[] | ["mocktioneer"] | Default bidders when not specified per-slot |
auto_configure | bool | true | Auto-remove client-side prebid.js scripts |
debug | bool | false | Enable Prebid debug mode (sets ext.prebid.debug and ext.prebid.returnallbidstatus) |
test_mode | bool | false | Set OpenRTB test: 1 for non-billable test traffic |
[integrations.aps]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable APS provider |
account_id | string | — | APS account ID (required; pub_id is an alias) |
endpoint | string | Built-in APS OpenRTB endpoint | Optional APS OpenRTB endpoint override |
timeout_ms | u32 | 800 | Request timeout |
debug | bool | false | Include the raw APS HTTP exchange in /auction provider metadata |
inventory_domain | string | — | Override site.domain for APS-authorized inventory |
inventory_page_origin | string | — | HTTPS origin paired with inventory_domain for site.page |
allow_script_creatives | bool | false | Admit script bids before APS candidate reduction |
[integrations.adserver_mock]
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable mediator |
endpoint | string | http://localhost:6767/adserver/mediate | Mediator service endpoint |
timeout_ms | u32 | 500 | Request timeout |
price_floor | f64? | null | Global price floor CPM |
Timeout Tuning
The orchestrator timeout should exceed the sum of provider timeouts to allow all providers to respond. Providers that exceed their individual timeouts are collected as they finish — the orchestrator doesn't wait indefinitely.
[auction]
timeout_ms = 2000 # Overall ceiling
[integrations.prebid]
timeout_ms = 1000 # Prebid Server budget
[integrations.aps]
timeout_ms = 800 # APS budget
[integrations.adserver_mock]
timeout_ms = 500 # Mediator budget (called after providers)Environment Variable Overrides
The typed ts config validate, ts config diff, and ts config push flows can override auction values that already exist in the TOML. EdgeZero v0.0.4 does not create missing leaves, so existing configs must add bothrewrite_creatives = true and sanitize_creatives = false under [auction] before relying on the corresponding environment overrides — an override for a missing leaf is silently ignored.
TRUSTED_SERVER__AUCTION__ENABLED=true
TRUSTED_SERVER__AUCTION__REWRITE_CREATIVES=true
TRUSTED_SERVER__AUCTION__SANITIZE_CREATIVES=false
TRUSTED_SERVER__AUCTION__PROVIDERS=prebid,aps
TRUSTED_SERVER__AUCTION__MEDIATOR=adserver_mock
TRUSTED_SERVER__AUCTION__TIMEOUT_MS=2000
TRUSTED_SERVER__INTEGRATIONS__PREBID__SERVER_URL=https://pbs.example.com
TRUSTED_SERVER__INTEGRATIONS__APS__ACCOUNT_ID=example-account
TRUSTED_SERVER__INTEGRATIONS__APS__DEBUG=falseBefore rolling back to a binary that does not know a creative-processing field, remove that field's non-default value (rewrite_creatives = false or sanitize_creatives = true), push the default-compatible blob, and then roll back. See Configuration for the complete migration, upgrade-sequencing, and rollback guidance.
Floor Prices
Floor prices can be set per-slot in the auction request. The orchestrator enforces floors after winner selection:
- In parallel_only mode: bids below the floor are dropped after selection
- In parallel_mediation mode: the floor is sent to the mediator in
ext.config.price_floor, and also enforced locally as a safety net - Bids without a decoded numeric price are dropped before delivery in both strategies
Error Handling
The orchestrator is designed to be resilient:
- Provider launch failure — If a provider fails to launch its request (e.g., missing backend), it is skipped with a warning. Other providers continue.
- Provider parse failure — If a response can't be parsed, an
AuctionResponse::error()is recorded. Other results are unaffected. - No providers configured — Returns an error:
"No providers configured" - All providers fail — Returns an empty
OrchestrationResultwith zero winning bids - Mediator returns bids without decoded prices — Those bids are filtered out with a warning
Observability
Logging
The auction system logs at multiple levels throughout execution:
| Level | Examples |
|---|---|
info | Auction request received, provider launch, bid counts, winner selection, total timing |
debug | Bid-drop reasons, mediation restoration notes, creative processing mode and byte counts |
warn | Provider launch failures, parse failures, mediator bids without decoded prices |
Response Metadata
Every auction response includes structured metadata in ext.orchestrator:
{
"strategy": "parallel_mediation",
"providers": 2,
"total_bids": 3,
"time_ms": 145
}