Real Time Bidding (RTB) Simulation#
This notebook implements a simplified Real Time Bidding (RTB) exchange using Flask. It demonstrates the process of creating a Bid Request (OpenRTB standard), soliciting bids from multiple DSPs, running a Second-Price Auction, and determining the winner.
How to Run This Code#
Install Requirements: Ensure you have Flask installed (
pip install flask).Run All Cells: Execute all cells in this notebook. The final cell starts the Flask server.
Access the Demo: Once the server is running, open your web browser and go to:
http://127.0.0.1:5003/test-rtb for the interactive demo.
Stop the Server: To stop the server, click the “Stop” button in the notebook toolbar or interrupt the kernel.
# Import necessary libraries
from flask import Flask, jsonify, request, render_template_string
import uuid
import time
from datetime import datetime
from typing import Dict, List, Optional, Any
import random
from dataclasses import dataclass, asdict
# Initialize Flask application
app = Flask(__name__)
# Data structures and Configuration
@dataclass
class Bid:
bidder_id: str
bidder_name: str
price: float
creative_id: str
targeting_match_score: float
# Mock DSP Configurations
rtb_bidders = [
{
"id": "dsp_premium",
"name": "Premium DSP",
"endpoint": "http://premium-dsp.com/bid", # Mock endpoint
"active": True,
"daily_budget": 10000.0,
"spent_today": 0.0,
"targeting": {
"countries": ["US", "CA", "UK", "AU"],
"languages": ["en"],
"devices": ["desktop", "mobile"],
"age_range": {"min": 25, "max": 54},
"interests": ["finance", "technology", "business"],
"min_viewability": 70,
},
"bidding": {
"base_cpm": 3.50,
"max_cpm": 15.00,
"bid_floor_multiplier": 1.2,
"targeting_boost": 0.3,
}
},
{
"id": "dsp_economy",
"name": "Economy DSP",
"active": True,
"daily_budget": 5000.0,
"spent_today": 0.0,
"targeting": {
"countries": ["US", "MX", "BR", "CA"],
"interests": ["general", "news", "viral"],
"devices": ["mobile"],
},
"bidding": {
"base_cpm": 1.50,
"max_cpm": 5.00,
"bid_floor_multiplier": 1.1,
"targeting_boost": 0.1,
}
}
]
class RTBExchange:
def __init__(self, bidders: List[Dict]):
self.bidders = bidders
self.publisher_id = "pub_001"
self.auction_history = []
def _get_device_type(self, device_str: str) -> int:
# OpenRTB Device Type mapping: 1=Mobile, 2=Desktop, 3=Tablet
mapping = {"mobile": 1, "desktop": 2, "tablet": 3}
return mapping.get(device_str, 2)
def create_bid_request(self, request_data: Dict) -> Dict:
"""Constructs an OpenRTB-style Bid Request object"""
bid_request_id = str(uuid.uuid4())
impression_id = str(uuid.uuid4())
width = 300
height = 250
# 1. User Object
user_obj = {
"id": request_data.get('user_id', 'anonymous_' + str(int(time.time()))),
"geo": {
"country": request_data.get('country', 'US'),
"region": request_data.get('region', 'CA'),
"city": request_data.get('city', 'San Francisco'),
"type": 2 # IP-derived location
},
"keywords": request_data.get('interests', [])
}
# 2. Device Object
device_obj = {
"devicetype": self._get_device_type(request_data.get('device', 'desktop')),
"ua": request_data.get('user_agent', 'Mozilla/5.0 (compatible; RTB-Bot/1.0)'),
"ip": request_data.get('ip', '192.168.1.1'),
"language": request_data.get('language', 'en'),
"os": request_data.get('os', 'Unknown')
}
# 3. Impression Object
impression_obj = {
"id": impression_id,
"banner": {
"w": width,
"h": height,
"format": [{"w": width, "h": height}],
"pos": request_data.get('ad_position', 1), # Above the fold
"topframe": 1 if request_data.get('top_frame', True) else 0
},
"bidfloor": request_data.get('floor_price', 0.50),
"bidfloorcur": "USD",
"secure": 1 if request_data.get('secure', True) else 0,
"tagid": request_data.get('ad_unit_id', 'default_unit')
}
# 4. Site Object
site_obj = {
"id": request_data.get('site_id', 'demo_site_001'),
"name": request_data.get('site_name', 'Demo Publisher Site'),
"domain": request_data.get('domain', 'demo-publisher.com'),
"cat": request_data.get('site_categories', ['IAB1']), # Arts & Entertainment
"page": request_data.get('page_url', 'https://demo-publisher.com/article'),
"publisher": {
"id": self.publisher_id,
"name": "Demo Publisher",
"cat": ["IAB1"]
}
}
# Combine into full Bid Request
return {
"id": bid_request_id,
"imp": [impression_obj],
"site": site_obj,
"user": user_obj,
"device": device_obj,
"tmax": 120 # Max time in ms
}
def simulate_bidder_bid(self, bidder: Dict, bid_request: Dict) -> Optional[Bid]:
"""Simulates a DSP receiving a request and deciding to bid"""
# 1. Check Budget
if bidder['spent_today'] >= bidder['daily_budget']:
return None
# 2. Check Country Targeting
user_country = bid_request['user']['geo']['country']
if user_country not in bidder['targeting']['countries']:
return None
# 3. Check Device Targeting
# Convert numeric OpenRTB type back to string for simple checking
device_type_map = {1: "mobile", 2: "desktop", 3: "tablet"}
req_device_type = device_type_map.get(bid_request['device']['devicetype'], "desktop")
if 'devices' in bidder['targeting'] and req_device_type not in bidder['targeting']['devices']:
return None
# 4. Calculate Bid Price
base_bid = bidder['bidding']['base_cpm']
# Calculate Match Score (Interest Overlap)
user_interests = set(bid_request['user'].get('keywords', []))
bidder_interests = set(bidder['targeting']['interests'])
overlap = len(user_interests & bidder_interests)
# Apply Targeting Boost
targeting_multiplier = 1.0 + (overlap * bidder['bidding']['targeting_boost'])
# Add Market Randomness
random_factor = random.uniform(0.9, 1.15)
final_price = base_bid * targeting_multiplier * random_factor
# Cap at Max CPM
final_price = min(final_price, bidder['bidding']['max_cpm'])
# Check Floor Price
floor = bid_request['imp'][0]['bidfloor']
if final_price < floor:
return None
return Bid(
bidder_id=bidder['id'],
bidder_name=bidder['name'],
price=round(final_price, 2),
creative_id=f"cr_{bidder['id']}_{int(time.time())}",
targeting_match_score=round(targeting_multiplier, 2)
)
def run_rtb_auction(self, request_data: Dict) -> Dict:
"""Orchestrates the RTB Auction process"""
# 1. Create Bid Request
bid_request = self.create_bid_request(request_data)
# 2. Solicit Bids from all DSPs
valid_bids = []
for bidder in self.bidders:
if not bidder['active']: continue
bid = self.simulate_bidder_bid(bidder, bid_request)
if bid:
valid_bids.append(bid)
# 3. Determine Winner
if not valid_bids:
return {
"auction_id": str(uuid.uuid4()),
"timestamp": datetime.utcnow().isoformat(),
"status": "no_bids",
"bid_request_summary": bid_request
}
# Sort by Effective Bid Value (Price * Quality Score)
sorted_bids = sorted(
valid_bids,
key=lambda x: x.price * x.targeting_match_score,
reverse=True
)
winning_bid = sorted_bids[0]
# 4. Calculate Clearing Price (Second Price Auction)
# Winner pays the price of the second highest bidder (or floor if only one)
if len(sorted_bids) > 1:
clearing_price = sorted_bids[1].price
else:
clearing_price = bid_request['imp'][0]['bidfloor']
# Ensure clearing price doesn't drop below floor even if 2nd bid was lower (unlikely given filter)
clearing_price = max(clearing_price, bid_request['imp'][0]['bidfloor'])
# 5. Update Spend (Simulation)
for b in self.bidders:
if b['id'] == winning_bid.bidder_id:
b['spent_today'] += clearing_price / 1000.0
# 6. Construct Result
result = {
"auction_id": str(uuid.uuid4()),
"timestamp": datetime.utcnow().isoformat(),
"status": "success",
"bid_request_id": bid_request['id'],
"winning_bidder": winning_bid.bidder_name,
"winning_price": winning_bid.price,
"clearing_price": clearing_price,
"user_country": bid_request['user']['geo']['country'],
"targeting_score": winning_bid.targeting_match_score,
"all_bids": [asdict(b) for b in valid_bids],
"bid_request_summary": {
"user_geo": bid_request['user']['geo'],
"interests": bid_request['user']['keywords']
}
}
self.auction_history.append(result)
return result
# Initialize Exchange
rtb_exchange = RTBExchange(rtb_bidders)
# API Routes
@app.route('/rtb/auction', methods=['POST'])
def run_rtb():
"""Run an RTB auction via API"""
try:
data = request.get_json() or {}
result = rtb_exchange.run_rtb_auction(data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/test-rtb')
def test_rtb_page():
"""Render a test page for RTB"""
html_template = """
<!DOCTYPE html>
<html>
<head>
<title>RTB Auction Simulator</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 40px; background: #f9f9f9; }
.container { max-width: 900px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
.controls { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
.form-group { margin-bottom: 10px; }
label { display: block; font-weight: bold; margin-bottom: 5px; color: #555; }
input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
button { background: #0066cc; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 16px; }
button:hover { background: #0052a3; }
.results { margin-top: 30px; border-top: 1px solid #eee; padding-top: 20px; }
.bid-card { border: 1px solid #eee; padding: 15px; margin-bottom: 10px; border-radius: 4px; background: #fff; }
.winner { border: 2px solid #28a745; background: #f0fff4; }
.clearing-price { color: #28a745; font-weight: bold; font-size: 1.2em; }
pre { background: #f4f4f4; padding: 10px; overflow-x: auto; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<h1>RTB Auction Simulator</h1>
<p>Simulate a Real-Time Bidding auction between a Premium DSP and an Economy DSP.</p>
<div class="controls">
<div>
<div class="form-group">
<label>User Country</label>
<select id="country">
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="UK">United Kingdom</option>
<option value="AU">Australia</option>
<option value="MX">Mexico</option>
</select>
</div>
<div class="form-group">
<label>Device Type</label>
<select id="device">
<option value="desktop">Desktop</option>
<option value="mobile">Mobile</option>
<option value="tablet">Tablet</option>
</select>
</div>
</div>
<div>
<div class="form-group">
<label>Interests (comma separated)</label>
<input type="text" id="interests" value="finance, technology" placeholder="finance, technology">
</div>
<div class="form-group">
<label>Floor Price ($)</label>
<input type="number" id="floor" value="0.50" step="0.10">
</div>
</div>
</div>
<button onclick="runAuction()">Run Auction</button>
<div id="output" class="results" style="display:none">
<h2>Auction Results</h2>
<div id="summary"></div>
<h3>Bidder Responses</h3>
<div id="bids-list"></div>
<h3>Full JSON Response</h3>
<pre id="json-debug"></pre>
</div>
</div>
<script>
async function runAuction() {
const payload = {
country: document.getElementById('country').value,
device: document.getElementById('device').value,
interests: document.getElementById('interests').value.split(',').map(s => s.trim()),
floor_price: parseFloat(document.getElementById('floor').value)
};
try {
const res = await fetch('/rtb/auction', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
renderResults(data);
} catch (e) {
alert('Error running auction: ' + e);
}
}
function renderResults(data) {
const output = document.getElementById('output');
const summary = document.getElementById('summary');
const bidsList = document.getElementById('bids-list');
const jsonDebug = document.getElementById('json-debug');
output.style.display = 'block';
jsonDebug.textContent = JSON.stringify(data, null, 2);
if (data.status === 'no_bids') {
summary.innerHTML = '<p style="color:red">No bidders participated in this auction (check targeting).</p>';
bidsList.innerHTML = '';
return;
}
summary.innerHTML = `
<div class="bid-card winner">
<h3>🏆 Winner: ${data.winning_bidder}</h3>
<p>Bid Price: $${data.winning_price.toFixed(2)}</p>
<p class="clearing-price">Clearing Price (2nd Price): $${data.clearing_price.toFixed(2)}</p>
<p>Targeting Score: ${data.targeting_score}</p>
</div>
`;
bidsList.innerHTML = data.all_bids.map(bid => {
const isWinner = bid.bidder_name === data.winning_bidder;
return `
<div class="bid-card ${isWinner ? 'winner' : ''}">
<strong>${bid.bidder_name}</strong>
<br>Bid: $${bid.price.toFixed(2)}
<br>Score: ${bid.targeting_match_score}
</div>
`;
}).join('');
}
</script>
</body>
</html>
"""
return render_template_string(html_template)
# Run the Flask application
if __name__ == '__main__':
# Run on port 5003 to avoid conflict with other examples
app.run(port=5003, debug=True, use_reloader=False)
* Serving Flask app '__main__'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5003
Press CTRL+C to quit
127.0.0.1 - - [27/Nov/2025 19:45:14] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:14] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:14] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:22] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:23] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:23] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:52] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:52] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [27/Nov/2025 19:45:55] "GET / HTTP/1.1" 404 -