Basic Ad Server

Basic Ad Server#

This notebook implements a simple programmatic ad server using Flask. It demonstrates ad targeting, bidding, and serving logic.

# Import necessary libraries
from flask import Flask, jsonify, request, render_template
from datetime import datetime
import json
from typing import Dict
import uuid

# Initialize Flask application
app = Flask(__name__)
# Sample ad inventory data structure
# Each ad contains metadata, targeting criteria, and performance statistics

ad_inventory = [
    {
        "id": 1,
        "name": "Summer Sale Banner",
        "image_url": "https://via.placeholder.com/300x250?text=Summer+Sale",
        "target_url": "https://example.com/summer-sale",
        "size": "300x250",
        "advertiser": "Fashion Store",
        "active": True,
        "targeting": {
            "countries": ["US", "CA", "UK"],  # Countries where ad can be shown
            "languages": ["en"],              # Supported languages
            "devices": ["desktop", "mobile"], # Target devices
            "age_range": {"min": 18, "max": 35}, # Target age range
            "interests": ["fashion", "shopping"], # Target user interests
            "daily_budget": 1000.00,          # Maximum daily spend in USD
            "cpm": 2.50                       # Cost per thousand impressions
        },
        "statistics": {
            "impressions": 0,                 # Number of times ad was shown
            "clicks": 0,                      # Number of clicks received
            "spent": 0.00                     # Total amount spent
        }
    },
    # Second ad entry with similar structure but different targeting
    {
        "id": 2,
        "name": "Tech Gadget Promotion",
        "image_url": "https://via.placeholder.com/728x90?text=Tech+Gadget",
        "target_url": "https://example.com/gadget-promo",
        "size": "728x90",
        "advertiser": "Tech Shop",
        "active": True,
        "targeting": {
            "countries": ["US", "DE", "FR"],
            "languages": ["en", "de", "fr"],
            "devices": ["desktop"],
            "age_range": {"min": 24, "max": 55},
            "interests": ["technology", "gadgets"],
            "daily_budget": 1500.00,
            "cpm": 3.00
        },
        "statistics": {
            "impressions": 0,
            "clicks": 0,
            "spent": 0.00
        }
    }
]
class AdTargeting:
    """Class handling ad targeting logic and bid calculations"""
    
    @staticmethod
    def match_targeting_criteria(ad: Dict, request_data: Dict) -> bool:
        """
        Check if an ad matches the targeting criteria based on request data
        Args:
            ad: Dictionary containing ad information and targeting rules
            request_data: Dictionary containing request parameters
        Returns:
            bool: True if ad matches all targeting criteria, False otherwise
        """
        targeting = ad['targeting']
        
        # Verify country match
        if request_data.get('country') not in targeting['countries']:
            return False
            
        # Verify language match
        if request_data.get('language') not in targeting['languages']:
            return False
            
        # Verify device type match
        if request_data.get('device') not in targeting['devices']:
            return False
            
        # Verify age requirements if provided
        user_age = request_data.get('age')
        if user_age and user_age.isdigit():
            user_age = int(user_age)
            if not (targeting['age_range']['min'] <= user_age <= targeting['age_range']['max']):
                return False
                
        # Check if daily budget has been exceeded
        if ad['statistics']['spent'] >= targeting['daily_budget']:
            return False
            
        return True

    @staticmethod
    def calculate_bid_value(ad: Dict, request_data: Dict) -> float:
        """
        Calculate the bid value for an ad based on targeting match quality
        Args:
            ad: Dictionary containing ad information
            request_data: Dictionary containing request parameters
        Returns:
            float: Calculated bid value
        """
        base_bid = ad['targeting']['cpm']
        
        # Apply premium inventory multiplier
        if request_data.get('premium'):
            base_bid *= 1.2
            
        # Calculate interest match multiplier
        user_interests = request_data.get('interests', [])
        if isinstance(user_interests, str):
            user_interests = [i.strip() for i in user_interests.split(',') if i.strip()]
        interest_match = len(set(user_interests) & set(ad['targeting']['interests']))
        interest_multiplier = 1 + (0.1 * interest_match)
        
        return base_bid * interest_multiplier
def track_impression(ad: Dict, bid_value: float):
    """
    Track ad impression and update statistics
    Args:
        ad: Dictionary containing ad information
        bid_value: Winning bid value for the impression
    Returns:
        str: Unique impression ID
    """
    impression_id = str(uuid.uuid4())
    impression = {
        "id": impression_id,
        "ad_id": ad['id'],
        "timestamp": datetime.utcnow().isoformat(),
        "ip": request.remote_addr,
        "bid_value": bid_value,
        "user_agent": request.headers.get('User-Agent')
    }
    
    # Update impression count and spent amount
    ad['statistics']['impressions'] += 1
    ad['statistics']['spent'] += bid_value / 1000  # Convert CPM to actual cost

    return impression_id
@app.route('/serve-ad', methods=['GET'])
def serve_ad():
    """
    API endpoint to serve an ad based on targeting criteria
    Query parameters:
        size: Ad size (e.g., '300x250')
        country: Target country code
        language: Target language code
        device: Device type
        age: User age
        interests: Comma-separated list of interests
        premium: Boolean indicating premium inventory
    Returns:
        JSON response with matched ad or error message
    """
    # Parse and normalize request parameters
    request_data = {
        'size': request.args.get('size', '300x250'),
        'country': request.args.get('country', 'US'),
        'language': request.args.get('language', 'en'),
        'device': request.args.get('device', 'desktop'),
        'age': int(request.args.get('age', 0)) if request.args.get('age') else None,
        'interests': request.args.get('interests', '').split(',') if request.args.get('interests') else [],
        'premium': request.args.get('premium', '').lower() == 'true'
    }
    
    # Find matching ads and calculate their bid values
    matching_ads = []
    for ad in ad_inventory:
        if (ad['size'] == request_data['size'] and 
            ad['active'] and 
            AdTargeting.match_targeting_criteria(ad, request_data)):
            
            bid_value = AdTargeting.calculate_bid_value(ad, request_data)
            matching_ads.append((ad, bid_value))
    
    # Return error if no matching ads found
    if not matching_ads:
        return jsonify({
            "error": "No matching ads found",
            "requested_criteria": request_data
        }), 404
    
    # Select highest bidding ad
    matching_ads.sort(key=lambda x: x[1], reverse=True)
    selected_ad, winning_bid = matching_ads[0]
    
    # Record impression and update statistics
    track_impression(selected_ad, winning_bid)
    
    # Remove sensitive targeting and statistics data
    ad_response = selected_ad.copy()
    ad_response.pop('targeting')
    ad_response.pop('statistics')
    
    return jsonify(ad_response)
@app.route('/ad-click/<int:ad_id>', methods=['GET'])
def track_click(ad_id):
    """
    Track ad clicks for a specific ad
    Args:
        ad_id: ID of the ad that was clicked
    Returns:
        JSON response indicating success or failure
    """
    ad = next((ad for ad in ad_inventory if ad['id'] == ad_id), None)
    if ad:
        ad['statistics']['clicks'] += 1
        return jsonify({"status": "success", "message": "Click tracked"})
    return jsonify({"status": "error", "message": "Ad not found"}), 404

@app.route('/ad-stats', methods=['GET'])
def get_statistics():
    """
    Get performance statistics for all ads
    Returns:
        JSON response with ad statistics including impressions, clicks, CTR, and spend
    """
    stats = [{
        "id": ad['id'],
        "name": ad['name'],
        "impressions": ad['statistics']['impressions'],
        "clicks": ad['statistics']['clicks'],
        "ctr": (ad['statistics']['clicks'] / ad['statistics']['impressions'] * 100) 
               if ad['statistics']['impressions'] > 0 else 0,
        "spent": ad['statistics']['spent']
    } for ad in ad_inventory]
    
    return jsonify(stats)

@app.route('/')
@app.route('/test')
def test_page():
    """Render the test page template"""
    # In a notebook, rendering templates from a folder might require configuration
    # or creating the file locally. For simplicity, we can return a string or simple HTML here if needed.
    try:
        return render_template('test.html')
    except:
        return "Test page. Ensure 'templates/test.html' exists."
# Run the Flask application
if __name__ == '__main__':
    # use_reloader=False is required for running Flask in Jupyter notebooks
    app.run(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:5000
Press CTRL+C to quit
127.0.0.1 - - [26/Nov/2025 19:17:42] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [26/Nov/2025 19:17:42] "GET /favicon.ico HTTP/1.1" 404 -