A flexible, extensible framework for managing multi-provider header bidding auctions with support for parallel execution and mediation.
The auction orchestration system allows you to:
- Run multiple auction providers (Prebid, Amazon APS, etc.) in parallel or sequentially
- Implement mediation strategies where a primary ad server makes the final decision
- Configure different auction flows for different scenarios
- Easily add new auction providers
┌─────────────────────────────────────────────────────────┐
│ Auction Orchestrator │
│ - Manages auction workflow & sequencing │
│ - Combines bids from multiple sources │
│ - Applies business logic │
└─────────────────────────────────────────────────────────┘
│
│ uses
▼
┌─────────────────────────────────────────────────────────┐
│ AuctionProvider Trait │
│ - request_bids() async │
│ - parse_response() │
│ - provider_name() │
│ - timeout_ms() │
│ - is_enabled() │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Prebid │ │ Amazon │ │ AdServer │
│ Provider │ │ APS │ │ Mock │
└──────────┘ └──────────┘ └──────────┘
When a request arrives at the /auction endpoint, it goes through the following steps:
┌──────────────────────────────────────────────────────────────────────┐
│ 1. HTTP POST /auction │
│ - Body: AdRequest (Prebid.js/tsjs format) │
│ - Headers: User-Agent, cookies, etc. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 2. Route Matching (crates/trusted-server-adapter-fastly/src/main.rs:84) │
│ - Pattern: (Method::POST, "/auction") │
│ - Handler: handle_auction(settings, &orchestrator, runtime_services, req)│
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 3. Parse Request Body (mod.rs:149) │
│ - Deserialize JSON → AdRequest struct │
│ - Extract ad units with media types │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 4. Generate User IDs (mod.rs:206-214) │
│ - Create/retrieve synthetic ID (persistent) │
│ - Generate fresh ID (per-request) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 5. Transform Request Format (mod.rs:216-240) │
│ - AdRequest → AuctionRequest │
│ - AdUnit.code → AdSlot.id │
│ - mediaTypes.banner.sizes → AdFormat[] │
│ - Build PublisherInfo, UserInfo, DeviceInfo │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 6. Use Provided Orchestrator (mod.rs:150) │
│ - Reused across requests from startup construction │
│ - Contains all registered providers (APS, Prebid, etc.) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 7. Create Auction Context (mod.rs:172-176) │
│ - Attach settings │
│ - Attach original request │
│ - Set timeout from config │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 8. Run Auction Strategy (orchestrator.rs:42) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Strategy: parallel_only │ │
│ │ 1. Launch all bidders concurrently │ │
│ │ 2. Wait for all responses │ │
│ │ 3. Select highest bid per slot │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Strategy: parallel_mediation │ │
│ │ 1. Launch all bidders concurrently │ │
│ │ 2. Collect all bids │ │
│ │ 3. Send to mediator for final decision │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 9. Each Provider Processes Request │
│ - Transform AuctionRequest → Provider format (e.g., APS TAM) │
│ - Send HTTP request to provider endpoint │
│ - Parse provider response │
│ - Transform → AuctionResponse with Bid[] │
│ - Return to orchestrator │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 10. Select Winning Bids (orchestrator.rs:363-385) │
│ - For each slot, find highest CPM bid │
│ - Create HashMap<slot_id, Bid> │
│ - Log winning selections │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 11. Transform to OpenRTB Response (mod.rs:274-322) │
│ - Build seatbid array (one per winning bid) │
│ - Rewrite creative HTML for first-party proxy │
│ - Add orchestrator metadata (timing, strategy, bid count) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 12. Return HTTP Response │
│ - Status: 200 OK │
│ - Content-Type: application/json │
│ - Body: OpenRTB BidResponse │
└──────────────────────────────────────────────────────────────────────┘
Client (browser, Prebid.js, tsjs) sends a POST request to /auction with ad unit definitions:
{
"adUnits": [
{
"code": "header-banner",
"mediaTypes": {
"banner": {
"sizes": [[728, 90], [970, 250]]
}
}
}
]
}The system transforms the Prebid.js format into an internal AuctionRequest:
// From: AdUnit with sizes [[728, 90], [970, 250]]
// To: AdSlot with formats
AdSlot {
id: "header-banner",
formats: vec![
AdFormat { width: 728, height: 90, media_type: Banner },
AdFormat { width: 970, height: 250, media_type: Banner },
],
floor_price: None,
targeting: HashMap::new(),
}Each registered provider (APS, Prebid, etc.) receives the AuctionRequest and:
- Transforms it to their specific format (e.g., APS TAM, OpenRTB)
- Makes HTTP request to their endpoint
- Parses the response
- Returns
AuctionResponsewithBid[]
For example, APS provider:
// Transform AuctionRequest → ApsBidRequest
let aps_request = ApsBidRequest {
pub_id: "5128",
slots: vec![
ApsSlot {
slot_id: "header-banner",
sizes: vec![[728, 90], [970, 250]],
slot_name: Some("header-banner"),
}
],
page_url: Some("https://example.com"),
ua: Some("Mozilla/5.0..."),
timeout: Some(800),
};
// HTTP POST to http://localhost:6767/e/dtb/bid
// Parse response → AuctionResponseThe orchestrator collects all bids and creates an OpenRTB response:
{
"id": "auction-response",
"seatbid": [
{
"seat": "amazon-aps",
"bid": [
{
"id": "amazon-aps-header-banner",
"impid": "header-banner",
"price": 2.5,
"adm": "<iframe src=\"/first-party/proxy?tsurl=...\">",
"w": 728,
"h": 90,
"crid": "amazon-aps-creative",
"adomain": ["amazon.com"]
}
]
}
],
"ext": {
"orchestrator": {
"strategy": "parallel_only",
"bidders": 1,
"total_bids": 1,
"time_ms": 5
}
}
}Note that creative HTML is rewritten to use the first-party proxy (/first-party/proxy) for privacy and security.
The trusted-server handles several types of routes defined in crates/trusted-server-adapter-fastly/src/main.rs:
| Route | Method | Handler | Purpose | Line |
|---|---|---|---|---|
/auction |
POST | handle_auction() |
Main auction endpoint (Prebid.js/tsjs format) | 162 |
/first-party/proxy |
GET | handle_first_party_proxy() |
Proxy creatives through first-party domain | 167 |
/first-party/click |
GET | handle_first_party_click() |
Track clicks on ads | 170 |
/first-party/sign |
GET/POST | handle_first_party_proxy_sign() |
Generate signed URLs for creatives | 173 |
/first-party/proxy-rebuild |
POST | handle_first_party_proxy_rebuild() |
Rebuild creative HTML with new settings | 176 |
/static/tsjs=* |
GET | handle_tsjs_dynamic() |
Serve tsjs library (Prebid.js alternative) | 145 |
/.well-known/trusted-server.json |
GET | handle_trusted_server_discovery() |
Public key distribution for request signing | 149 |
/verify-signature |
POST | handle_verify_signature() |
Verify signed requests | 154 |
/admin/keys/rotate |
POST | handle_rotate_key() |
Rotate signing keys (admin only) | 158 |
/admin/keys/deactivate |
POST | handle_deactivate_key() |
Deactivate signing keys (admin only) | 159 |
/integrations/* |
* | Integration Registry | Provider-specific endpoints (Prebid, etc.) | 179 |
* (fallback) |
* | handle_publisher_request() |
Proxy to publisher origin | 195 |
The Fastly Compute entrypoint uses pattern matching on (Method, path) tuples:
let result = match (method, path.as_str()) {
(Method::GET, path) if path.starts_with("/static/tsjs=") => {
handle_tsjs_dynamic(&req, integration_registry)
}
(Method::GET, "/.well-known/trusted-server.json") => {
handle_trusted_server_discovery(settings, runtime_services, req)
}
(Method::POST, "/verify-signature") => handle_verify_signature(settings, req),
(Method::POST, "/admin/keys/rotate") => handle_rotate_key(settings, req),
(Method::POST, "/admin/keys/deactivate") => handle_deactivate_key(settings, req),
(Method::POST, "/auction") => {
handle_auction(settings, orchestrator, runtime_services, req).await
}
(Method::GET, "/first-party/proxy") => {
handle_first_party_proxy(settings, runtime_services, req).await
}
(Method::GET, "/first-party/click") => {
handle_first_party_click(settings, runtime_services, req).await
}
(Method::GET, "/first-party/sign") | (Method::POST, "/first-party/sign") => {
handle_first_party_proxy_sign(settings, runtime_services, req).await
}
(Method::POST, "/first-party/proxy-rebuild") => {
handle_first_party_proxy_rebuild(settings, runtime_services, req).await
}
(m, path) if integration_registry.has_route(&m, path) => integration_registry
.handle_proxy(&m, path, settings, runtime_services, req)
.await
.unwrap_or_else(|| {
Err(Report::new(TrustedServerError::BadRequest {
message: format!("Unknown integration route: {path}"),
}))
}),
_ => handle_publisher_request(settings, integration_registry, runtime_services, req),
};Some integrations register their own routes dynamically. For example, Prebid registers /integrations/prebid/auction:
// In integrations/prebid.rs
impl Integration for PrebidIntegration {
fn routes(&self) -> Vec<IntegrationRoute> {
vec![
IntegrationRoute {
path: "/integrations/prebid/auction",
method: Method::POST,
handler: handle_prebid_auction,
}
]
}
}The integration registry checks if a route matches any registered integration routes before falling back to the publisher origin.
Routes are matched in this order:
- Exact top-level routes (
/auction,/first-party/proxy, etc.) - Admin routes (
/admin/*) - Integration routes (
/integrations/*) - Fallback to publisher origin (all other paths)
This ensures auction and first-party endpoints take precedence over publisher content.
The /auction endpoint is the primary entry point for auctions:
Input Format (Prebid.js compatible):
{
"adUnits": [
{
"code": "div-id",
"mediaTypes": {
"banner": {
"sizes": [[300, 250], [728, 90]]
}
}
}
],
"config": { /* optional Prebid.js config */ }
}Output Format (OpenRTB 2.x):
{
"id": "auction-response",
"seatbid": [
{
"seat": "bidder-name",
"bid": [
{
"id": "bid-id",
"impid": "div-id",
"price": 2.5,
"adm": "<creative-html>",
"w": 300,
"h": 250
}
]
}
],
"ext": {
"orchestrator": {
"strategy": "parallel_only",
"bidders": 2,
"total_bids": 3,
"time_ms": 150
}
}
}Key Transformations:
adUnits[].code→seatbid[].bid[].impid(slot identifier)mediaTypes.banner.sizes→ evaluated by providers, winning size inbid.wandbid.h- Creative HTML is rewritten to use
/first-party/proxyURLs - Multiple bids per slot become separate
seatbidentries - Orchestrator metadata added in
ext.orchestrator
Implements the AuctionProvider trait to integrate with a specific SSP/ad exchange.
A named configuration that defines:
- Which providers participate
- Execution strategy (parallel mediation or parallel only)
- Timeout settings
- Optional mediator
Manages the execution of an auction flow, coordinates providers, and collects results.
Use case: Header bidding with ad server mediation
[auction]
enabled = true
providers = ["prebid", "aps"]
mediator = "adserver_mock" # Setting mediator enables parallel mediation strategy
timeout_ms = 2000Flow:
- Prebid and APS run in parallel
- Both return their bids simultaneously
- Bids are sent to the mediator for final decision
- Mediator competes house inventory and returns winning creative
Use case: Client-side auction, no mediation
[auction]
enabled = true
providers = ["prebid", "aps"]
# No mediator = parallel only strategy (highest CPM wins)
timeout_ms = 2000Flow:
- All providers run in parallel
- Highest bid wins
- No mediation server involved
All auction settings are configured directly under [auction]:
[auction]
enabled = true # Enable/disable auction orchestration
providers = ["prebid", "aps"] # List of bidder providers
mediator = "adserver_mock" # Optional: if set, uses mediation; if omitted, highest bid wins
timeout_ms = 2000 # Overall auction timeoutStrategy Auto-Detection:
- When
mediatoris configured → Runs parallel mediation (providers in parallel, mediator decides winner) - When
mediatoris omitted → Runs parallel only (providers in parallel, highest CPM wins)
Each provider has its own configuration section:
[integrations.prebid]
enabled = true
server_url = "https://prebid-server.example.com"
timeout_ms = 1000
[integrations.aps]
enabled = true
mock = true # Set to false for real integration
timeout_ms = 800
[integrations.adserver_mock]
enabled = true
endpoint = "http://localhost:6767/adserver/mediate"
timeout_ms = 500- Create a new file in
src/auction/providers/your_provider.rs
use async_trait::async_trait;
use crate::auction::provider::AuctionProvider;
use crate::auction::types::{AuctionContext, AuctionRequest, AuctionResponse};
use crate::platform::{PlatformPendingRequest, PlatformResponse};
pub struct YourAuctionProvider {
config: YourConfig,
}
#[async_trait(?Send)]
impl AuctionProvider for YourAuctionProvider {
fn provider_name(&self) -> &'static str {
"your_provider"
}
async fn request_bids(
&self,
request: &AuctionRequest,
_context: &AuctionContext<'_>,
) -> Result<PlatformPendingRequest, Report<TrustedServerError>> {
// 1. Transform AuctionRequest to your provider's format
// 2. Launch HTTP request through services.http_client().send_async(...)
// 3. Return PlatformPendingRequest for the orchestrator to await
todo!()
}
fn parse_response(
&self,
response: PlatformResponse,
response_time_ms: u64,
) -> Result<AuctionResponse, Report<TrustedServerError>> {
// 4. Parse PlatformResponse into AuctionResponse
todo!()
}
fn timeout_ms(&self) -> u32 {
self.config.timeout_ms
}
fn is_enabled(&self) -> bool {
self.config.enabled
}
}-
Register the provider in
src/auction/providers/mod.rs -
Configure it in
trusted-server.toml
APS and adserver_mock providers are used for testing the orchestration pattern:
- APS Mock: Returns synthetic bids with Amazon branding
- AdServer Mock: Acts as mediator by calling mocktioneer's mediation endpoint, selects winning bids based on highest CPM
Set mock = false in APS config when real APS integration is ready.
let orchestrator = AuctionOrchestrator::new(config);
orchestrator.register_provider(Arc::new(PrebidAuctionProvider::new(prebid_config)));
orchestrator.register_provider(Arc::new(ApsAuctionProvider::new(aps_config)));
let result = orchestrator.run_auction(&request, &context, &services).await?;
// Check results
assert_eq!(result.winning_bids.len(), 2);
assert!(result.total_time_ms < 2000);- Parallel Execution: Currently runs sequentially in Fastly Compute (no tokio runtime), but structured for easy parallelization
- Timeouts: Each provider has independent timeout; global timeout enforced at flow level
- Error Handling: Provider failures don't fail entire auction; partial results returned
src/auction/mod.rs- Module exportssrc/auction/types.rs- Core auction typessrc/auction/provider.rs- Provider trait definitionsrc/auction/orchestrator.rs- Orchestration logicsrc/auction/config.rs- Configuration typessrc/auction/providers/- Provider implementations
See the main project README or integration guide.