Real Time Bidding (RTB) Simulation

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#

  1. Install Requirements: Ensure you have Flask installed (pip install flask).

  2. Run All Cells: Execute all cells in this notebook. The final cell starts the Flask server.

  3. Access the Demo: Once the server is running, open your web browser and go to:

  4. 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 -