Expert guides, insights and articles updated for 2026
Published 3 hours ago
Real-time bidding often gets explained with diagrams but not enough working code. You hear terms like OpenRTB, bid request, seatbid, and win notice, yet the actual request-response loop stays abstract. That gap is where most of the confusion starts.
If you want to build a real-time bidder in Python, the key idea is simple: RTB is a latency-constrained decision loop. A request arrives, you inspect the impression opportunity, decide whether to buy it, and respond before the deadline runs out.
This guide walks through a minimal Python bidder you can run locally. It accepts an OpenRTB-style request, applies simple bidding rules, returns either a bid or a no-bid, and logs win notices.
This is a learning prototype, not a production DSP. It leaves out pacing, creative workflows, identity, attribution, and large-scale infrastructure. What it gives you instead is a working foundation you can inspect and extend.
Before opening Python, get the auction flow straight. Most implementation mistakes come from misunderstanding what the bidder must do under time pressure.
A basic RTB interaction looks like this:
That is the core loop.
A useful mental model is to treat each request as a tiny buying decision with an expiration timer attached. You are not browsing inventory. You are making a fast yes-or-no decision on one impression at a time.
| Actor | Role | What they send |
|---|---|---|
| Exchange / SSP | Offers auction opportunities | Bid request |
| Bidder | Evaluates and bids | Bid response or no-bid |
| Advertiser logic | Defines goals and constraints | Campaign rules, bid caps, targeting |
| Tracking / analytics | Records outcomes | Win, impression, click, conversion logs |
In this tutorial, your Python app is the bidder. Advertiser logic is just a few hard-coded rules.
In many systems, you can afford extra validation, a database lookup, or another API call. In RTB, those choices can make your bid miss the auction.
The tmax field in OpenRTB defines the exchange’s maximum response window.[^1] Even if tmax is 120 milliseconds, your app does not get all of it. Network overhead and exchange-side processing consume part of that budget.
Key Insight: In RTB, a correct answer that arrives late is effectively the same as no answer.
That is why a fast no-bid is often better than a slow bid.
Bottom Line: RTB is best understood as a deadline-driven decision engine, not just an auction format.
A first bidder only needs to handle five responsibilities.
Your bidder needs an endpoint, usually POST /bid, that accepts JSON.
You do not need the full specification to get started. You only need enough data to answer one question:
Should I bid on this impression, and if so, how much?
This is the core decision function. In a first version, use transparent rules rather than machine learning.
If the request qualifies, return JSON that references the right impression ID and includes price and creative details.
A local /win endpoint is enough to show the feedback loop. Even simple logging teaches the right architecture.
Decision Rule: If your first bidder can receive, evaluate, respond, and record, it already covers the essential system boundaries.
The OpenRTB spec is broad.[^1] A beginner build should focus on a small subset.
| Field | Why it matters | Notes |
|---|---|---|
id |
Identifies the auction request | Echo this in the response |
imp |
Contains one or more impression opportunities | Usually the most important object |
imp[].id |
Identifies the impression | Used as impid in the bid |
imp[].bidfloor |
Minimum acceptable bid | Do not bid below it |
site or app |
Shows where the impression comes from | This tutorial uses site |
device |
Helps with targeting | Device type, user agent, geo hints |
user |
Optional user context | Often sparse or privacy-limited |
tmax |
Response deadline budget | Critical for timeout logic |
| Field | Why it matters | Notes |
|---|---|---|
id |
Ties response to request | Usually set to request ID |
seatbid |
Holds bids | Top-level array |
seatbid[].bid |
Actual bid list | At least one bid to participate |
bid.id |
Unique bid ID | Can be generated locally |
bid.impid |
Ties bid to impression | Must match imp[].id |
bid.price |
Bid amount | Must respect floor |
bid.adm |
Ad markup | Placeholder only in this demo |
bid.crid |
Creative ID | Hard-coded is fine for a demo |
bid.nurl |
Win notice URL in some integrations | Exchange-specific; verify docs |
impA request may contain multiple imp objects. Each one is a possible auction opportunity.
In production, you might evaluate several impressions with different pricing or creative logic. In this tutorial, we keep it simple and bid on the first compatible impression.
That is not a best practice for every case. It is just a useful simplification for learning.
If a field is optional and your bidder does not need it, ignore it.
Guessing is worse than skipping. For example, if you do not know how an exchange expects adm or notification fields to behave, do not invent production assumptions. Use clearly labeled demo placeholders and check the partner documentation later.[^1]
Common Mistake: Trying to model the whole OpenRTB schema before building the first working request-response loop.
Bottom Line: Start with the fields that affect eligibility, pricing, and response validity. Add the rest later.
To keep the system understandable, use this four-part framework throughout the build.
| Stage | What it does | What can go wrong |
|---|---|---|
| Receive | Accept JSON, validate shape, normalize fields | Malformed JSON, missing imp, missing id |
| Evaluate | Apply fast bidding rules | Slow logic, bad targeting assumptions, ignored floors |
| Respond | Return a compliant bid or no-bid | Invalid JSON, wrong impid, late response |
| Record | Log decisions and outcomes | No debugging trail, no learning loop |
This framework maps directly to how a real bidder behaves under load.
Validate just enough to avoid crashing or bidding blindly.
A first bidder should use rules you can explain easily. That makes testing and debugging far simpler.
A no-bid is a valid outcome, not a failure.
Without logs, you cannot improve targeting, pricing, or win rate later.
Bottom Line: Keeping these four stages separate makes the bidder easier to test, debug, and extend.
For readability, this guide uses FastAPI[^2] and Uvicorn[^3].
You could use Flask, but FastAPI keeps typed request handling cleaner for this kind of tutorial.
rtb-bidder/
├── app/
│ └── main.py
├── sample_payloads/
│ ├── bid_request_us_mobile.json
│ └── bid_request_nobid.json
└── requirements.txt
Create requirements.txt:
fastapi
uvicorn
pydantic
Install them:
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
app/main.pyCheckpoint: If you can run a FastAPI app locally, you are ready to build the bidder.
The first goal is modest: accept a request safely and fail cleanly.
app/main.pyimport time
import uuid
import logging
from typing import Any, Dict, Optional
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rtb_bidder")
app = FastAPI(title="Minimal Python RTB Bidder")
ALLOWED_COUNTRIES = {"USA", "US"}
ALLOWED_PUBLISHERS = {"example.com", "news.example.com"}
MAX_BID_CPM = 1.35
MAX_ACCEPTABLE_FLOOR = 1.20
INTERNAL_TMAX_CAP_MS = 100
class BidDecision(BaseModel):
should_bid: bool
reason: str
impid: Optional[str] = None
price: Optional[float] = None
creative_id: Optional[str] = None
def get_country(payload: Dict[str, Any]) -> Optional[str]:
device = payload.get("device", {}) or {}
geo = device.get("geo", {}) or {}
return geo.get("country")
def get_domain(payload: Dict[str, Any]) -> Optional[str]:
site = payload.get("site", {}) or {}
return site.get("domain")
def is_mobile_web(payload: Dict[str, Any]) -> bool:
site = payload.get("site")
app_obj = payload.get("app")
if not site or app_obj:
return False
device = payload.get("device", {}) or {}
devicetype = device.get("devicetype")
ua = (device.get("ua") or "").lower()
mobile_by_type = devicetype in {1, 4, 5}
mobile_by_ua = "iphone" in ua or "android" in ua or "mobile" in ua
return mobile_by_type or mobile_by_ua
def choose_impression(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
impressions = payload.get("imp", [])
if not isinstance(impressions, list):
return None
for imp in impressions:
if isinstance(imp, dict) and imp.get("id"):
return imp
return None
This is not a full OpenRTB schema. It is minimal structural validation.
That matters because exchange payloads are often sparse, and different integrations add different extensions.
Now add the evaluation logic:
def evaluate_request(payload: Dict[str, Any], deadline_ms: float) -> BidDecision:
now_ms = time.time() * 1000
if now_ms >= deadline_ms:
return BidDecision(should_bid=False, reason="timeout_before_evaluation")
request_id = payload.get("id")
if not request_id:
return BidDecision(should_bid=False, reason="missing_request_id")
imp = choose_impression(payload)
if not imp:
return BidDecision(should_bid=False, reason="missing_impression")
impid = imp.get("id")
bidfloor = float(imp.get("bidfloor", 0.0) or 0.0)
country = get_country(payload)
domain = get_domain(payload)
if country not in ALLOWED_COUNTRIES:
return BidDecision(should_bid=False, reason="geo_not_allowed")
if not is_mobile_web(payload):
return BidDecision(should_bid=False, reason="not_mobile_web")
if domain not in ALLOWED_PUBLISHERS:
return BidDecision(should_bid=False, reason="publisher_not_allowed")
if bidfloor > MAX_ACCEPTABLE_FLOOR:
return BidDecision(should_bid=False, reason="floor_too_high")
now_ms = time.time() * 1000
if now_ms >= deadline_ms:
return BidDecision(should_bid=False, reason="timeout_after_filters")
bid_price = max(bidfloor, MAX_BID_CPM)
return BidDecision(
should_bid=True,
reason="eligible",
impid=impid,
price=round(bid_price, 2),
creative_id="creative-001"
)
Decision Rule: If the request is malformed or missing essentials, return no-bid quickly instead of trying to infer meaning.
Now make the parsing logic explicit.
The most important parts of the request are:
idimp[].idimp[].bidfloorsite.domaindevice.uadevice.devicetypedevice.geo.countrytmaxThat is enough for a first policy such as:
“Bid only on US mobile web traffic from approved publishers if the floor is acceptable.”
This prototype uses choose_impression() to pick the first valid impression.
That is a deliberate simplification. Real implementations may evaluate several impressions or return multiple bids depending on the integration.
Notice how the helper functions default to empty dicts. That keeps the bidder from crashing when site, device, or geo is missing.
A common beginner mistake is assuming every request has the same shape. In RTB, sparse payloads are normal.
Bottom Line: Defensive parsing matters more than complete parsing in a first bidder.
This is where many tutorials get too ambitious. Do not start with machine learning. Start with a rule you can explain in one sentence.
Our demo strategy is:
bidfloor <= 1.20These values are illustrative, not market guidance.
Imagine a startup advertiser that only wants mobile web visitors from a small set of known publishers because those placements have already been tested.
That is realistic. It is narrow enough to debug and selective enough to make no-bid decisions common.
If the floor is 1.50 and your max acceptable floor is 1.20, do not bid.
If the floor is 0.80, the demo bidder can return 1.35.
This works because:
Return no-bid when:
Key Insight: A healthy bidder is selective. Most opportunities should be rejected quickly if they do not fit the strategy.
Now turn an eligible decision into a valid response.
def build_bid_response(request_id: str, decision: BidDecision) -> Dict[str, Any]:
return {
"id": request_id,
"seatbid": [
{
"seat": "demo-seat",
"bid": [
{
"id": str(uuid.uuid4()),
"impid": decision.impid,
"price": decision.price,
"crid": decision.creative_id,
"adm": "<div>Demo Creative</div>",
"nurl": f"http://localhost:8000/win?impid={decision.impid}&price=${{AUCTION_PRICE}}"
}
]
}
]
}
For display traffic, markup and macros can vary a lot across exchanges and media types. This example teaches response structure, not creative compliance.
@app.post("/bid")
async def bid_endpoint(request: Request):
start_ms = time.time() * 1000
try:
payload = await request.json()
except Exception:
logger.info("invalid_json")
return Response(status_code=204)
tmax = payload.get("tmax", INTERNAL_TMAX_CAP_MS)
try:
tmax = int(tmax)
except Exception:
tmax = INTERNAL_TMAX_CAP_MS
effective_tmax = min(tmax, INTERNAL_TMAX_CAP_MS)
deadline_ms = start_ms + effective_tmax
decision = evaluate_request(payload, deadline_ms)
logger.info(
"request_id=%s should_bid=%s reason=%s",
payload.get("id"),
decision.should_bid,
decision.reason
)
if not decision.should_bid:
# Some exchanges prefer 204 No Content for no-bid.
# Verify your partner's expected behavior.
return Response(status_code=204)
response_payload = build_bid_response(payload["id"], decision)
return JSONResponse(content=response_payload)
Common Mistake: Returning malformed JSON, using the wrong
impid, or bidding below the floor. Any one of those can get the bid rejected.
Checkpoint: Your bidder now has a complete request → decision → response path.
A bidder that ignores time budgets is not really an RTB bidder.
tmaxtmax is the maximum response time budget sent in the request.[^1] It is not a suggestion.
In practice, your internal deadline should be lower than the full tmax, because the total round trip includes more than your Python code.
This tutorial uses:
INTERNAL_TMAX_CAP_MS = 100That is the right shape for a first prototype.
Do not try to rescue a request that is already timing out. Return no-bid.
A late bid wastes resources and creates misleading logs. You think you participated, but the exchange may never have considered the bid.
Decision Rule: If you are close to the deadline and still uncertain, no-bid is the safer choice.
Winning matters because it closes the loop.
If your bid wins, some integrations send a callback or fire a notification URL. That tells your bidder which bids actually cleared the auction.
In production, you would store win events for optimization, reconciliation, and budget tracking.
For a local prototype, logging is enough:
@app.get("/win")
async def win_notice(impid: Optional[str] = None, price: Optional[str] = None):
logger.info("win_notice impid=%s price=%s", impid, price)
return {"status": "logged", "impid": impid, "price": price}
This is simple, but it teaches the right concept: your bidder should not stop caring after it sends a bid.
Bottom Line: Bid submission is not the end of the system. Outcome logging is part of the architecture.
import time
import uuid
import logging
from typing import Any, Dict, Optional
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rtb_bidder")
app = FastAPI(title="Minimal Python RTB Bidder")
ALLOWED_COUNTRIES = {"USA", "US"}
ALLOWED_PUBLISHERS = {"example.com", "news.example.com"}
MAX_BID_CPM = 1.35
MAX_ACCEPTABLE_FLOOR = 1.20
INTERNAL_TMAX_CAP_MS = 100
class BidDecision(BaseModel):
should_bid: bool
reason: str
impid: Optional[str] = None
price: Optional[float] = None
creative_id: Optional[str] = None
def get_country(payload: Dict[str, Any]) -> Optional[str]:
device = payload.get("device", {}) or {}
geo = device.get("geo", {}) or {}
return geo.get("country")
def get_domain(payload: Dict[str, Any]) -> Optional[str]:
site = payload.get("site", {}) or {}
return site.get("domain")
def is_mobile_web(payload: Dict[str, Any]) -> bool:
site = payload.get("site")
app_obj = payload.get("app")
if not site or app_obj:
return False
device = payload.get("device", {}) or {}
devicetype = device.get("devicetype")
ua = (device.get("ua") or "").lower()
mobile_by_type = devicetype in {1, 4, 5}
mobile_by_ua = "iphone" in ua or "android" in ua or "mobile" in ua
return mobile_by_type or mobile_by_ua
def choose_impression(payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
impressions = payload.get("imp", [])
if not isinstance(impressions, list):
return None
for imp in impressions:
if isinstance(imp, dict) and imp.get("id"):
return imp
return None
def evaluate_request(payload: Dict[str, Any], deadline_ms: float) -> BidDecision:
now_ms = time.time() * 1000
if now_ms >= deadline_ms:
return BidDecision(should_bid=False, reason="timeout_before_evaluation")
request_id = payload.get("id")
if not request_id:
return BidDecision(should_bid=False, reason="missing_request_id")
imp = choose_impression(payload)
if not imp:
return BidDecision(should_bid=False, reason="missing_impression")
impid = imp.get("id")
bidfloor = float(imp.get("bidfloor", 0.0) or 0.0)
country = get_country(payload)
domain = get_domain(payload)
if country not in ALLOWED_COUNTRIES:
return BidDecision(should_bid=False, reason="geo_not_allowed")
if not is_mobile_web(payload):
return BidDecision(should_bid=False, reason="not_mobile_web")
if domain not in ALLOWED_PUBLISHERS:
return BidDecision(should_bid=False, reason="publisher_not_allowed")
if bidfloor > MAX_ACCEPTABLE_FLOOR:
return BidDecision(should_bid=False, reason="floor_too_high")
now_ms = time.time() * 1000
if now_ms >= deadline_ms:
return BidDecision(should_bid=False, reason="timeout_after_filters")
bid_price = max(bidfloor, MAX_BID_CPM)
return BidDecision(
should_bid=True,
reason="eligible",
impid=impid,
price=round(bid_price, 2),
creative_id="creative-001"
)
def build_bid_response(request_id: str, decision: BidDecision) -> Dict[str, Any]:
return {
"id": request_id,
"seatbid": [
{
"seat": "demo-seat",
"bid": [
{
"id": str(uuid.uuid4()),
"impid": decision.impid,
"price": decision.price,
"crid": decision.creative_id,
"adm": "<div>Demo Creative</div>",
"nurl": f"http://localhost:8000/win?impid={decision.impid}&price=${{AUCTION_PRICE}}"
}
]
}
]
}
@app.post("/bid")
async def bid_endpoint(request: Request):
start_ms = time.time() * 1000
try:
payload = await request.json()
except Exception:
logger.info("invalid_json")
return Response(status_code=204)
tmax = payload.get("tmax", INTERNAL_TMAX_CAP_MS)
try:
tmax = int(tmax)
except Exception:
tmax = INTERNAL_TMAX_CAP_MS
effective_tmax = min(tmax, INTERNAL_TMAX_CAP_MS)
deadline_ms = start_ms + effective_tmax
decision = evaluate_request(payload, deadline_ms)
logger.info(
"request_id=%s should_bid=%s reason=%s",
payload.get("id"),
decision.should_bid,
decision.reason
)
if not decision.should_bid:
return Response(status_code=204)
response_payload = build_bid_response(payload["id"], decision)
return JSONResponse(content=response_payload)
@app.get("/win")
async def win_notice(impid: Optional[str] = None, price: Optional[str] = None):
logger.info("win_notice impid=%s price=%s", impid, price)
return {"status": "logged", "impid": impid, "price": price}
get_country() extracts country from device.geoget_domain() reads the publisher domain from siteis_mobile_web() distinguishes mobile web from app or desktopchoose_impression() selects a compatible impressionevaluate_request() applies bidding rulesbuild_bid_response() formats a minimal response/bid handles the auction request/win logs win noticesSave as sample_payloads/bid_request_us_mobile.json:
{
"id": "req-1001",
"imp": [
{
"id": "imp-1",
"bidfloor": 0.8
}
],
"site": {
"domain": "example.com",
"page": "https://example.com/article/123"
},
"device": {
"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148",
"devicetype": 4,
"geo": {
"country": "USA"
}
},
"tmax": 120
}
{
"id": "req-1001",
"seatbid": [
{
"seat": "demo-seat",
"bid": [
{
"id": "f3d2e86c-4f6c-4e8a-a6f8-11f7f0f2bdb2",
"impid": "imp-1",
"price": 1.35,
"crid": "creative-001",
"adm": "<div>Demo Creative</div>",
"nurl": "http://localhost:8000/win?impid=imp-1&price=${AUCTION_PRICE}"
}
]
}
]
}
Save as sample_payloads/bid_request_nobid.json:
{
"id": "req-1002",
"imp": [
{
"id": "imp-2",
"bidfloor": 1.5
}
],
"site": {
"domain": "example.com",
"page": "https://example.com/article/999"
},
"device": {
"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148",
"devicetype": 4,
"geo": {
"country": "USA"
}
},
"tmax": 120
}
This one should return no-bid because the floor is above the configured threshold.
uvicorn app.main:app --reload
curl -X POST "http://127.0.0.1:8000/bid" \
-H "Content-Type: application/json" \
--data @sample_payloads/bid_request_us_mobile.json
curl -i -X POST "http://127.0.0.1:8000/bid" \
-H "Content-Type: application/json" \
--data @sample_payloads/bid_request_nobid.json
| Scenario | Expected result | Why |
|---|---|---|
| US mobile web, allowed domain, floor 0.8 | Bid response | Meets all rules |
| US mobile web, allowed domain, floor 1.5 | No-bid | Floor too high |
| Canada mobile web | No-bid | Geo not allowed |
| Desktop traffic | No-bid | Not mobile web |
| Unknown publisher | No-bid | Not in allowlist |
Content-Type: application/json is setidimp is present and is a listimp[].id existsimpid matches the request impressionbidfloorCommon Mistake: Treating a 204 no-bid as an error. In many integrations, that is the normal and preferred no-bid signal. Always confirm partner docs.
If the design depends on slow calls in the hot path, it will eventually miss auctions.
OpenRTB payloads are often sparse. Your parser needs to tolerate missing optional fields.
A valid strategy with an invalid response is still a failed bid.
A bidder should reject impossible or unqualified auctions immediately.
This tutorial is intentionally educational. A real bidder needs much more than a clean local API.
Bottom Line: The fastest path to a useful first bidder is disciplined simplification.
A working local bidder is useful, but still far from a production traffic-buying system.
Decision Rule: If your goal is learning or controlled experimentation, this prototype is enough. If your goal is dependable spend at scale, it is not.
Not every advertiser or startup should build RTB infrastructure.
| Reason | Why it makes sense |
|---|---|
| You want to learn RTB deeply | Building exposes the real mechanics |
| You need custom optimization logic | Managed tools may be too opinionated |
| You are testing a niche traffic strategy | A custom prototype can validate assumptions quickly |
| You need full control over auction decisions | Internal logic stays transparent |
| Reason | Why it often wins |
|---|---|
| You need speed to market | Infrastructure already exists |
| You do not want to maintain bidder operations | Lower engineering burden |
| You need compliance and operational support | Managed providers handle more edge cases |
| You care more about outcomes than system ownership | Better fit for most advertisers |
If your real goal is not to run bidding infrastructure but to buy traffic efficiently, managed services may be the better option.
For advertisers and business owners who want acquisition outcomes without maintaining their own RTB stack, Traffics.io can be a practical fit. That is especially true when the cost of building, maintaining, and integrating a bidder outweighs the value of owning the system.
Bottom Line: Build when learning or custom logic is the priority. Buy when reliability, speed, and operational simplicity matter more.
Once the first version works, do not add everything at once. Add the next constraint that matters.
Move from hard-coded constants to campaign configuration.
Examples:
Even simple persistence changes the system. Once you record outcomes, you can evaluate:
This is where a bidder starts acting like a buying system instead of a request parser.
Only after you have data should you consider models for:
Practical Next Step: Refactor the constants into a config file or campaign object before adding more logic. That one change will make future growth much easier.
The main takeaway is simpler than the jargon suggests: a bidder receives an auction opportunity, evaluates it under a deadline, responds with a bid or no-bid, and records the outcome.
You now have a working foundation for a real-time bidder in Python. More importantly, you should understand why each part exists: imp represents the opportunity, bidfloor shapes pricing, tmax shapes architecture, no-bid is normal, and win notices close the loop.
From here, the most useful next steps are campaign configuration, event storage, pacing, and richer targeting. Once those are in place, you are no longer just returning JSON. You are starting to build real traffic-buying logic.
A real-time bidder in Python is an application that receives ad auction requests, evaluates them under strict time limits, and returns either a bid or a no-bid. In practice, it usually processes OpenRTB bid requests over HTTP and applies simple or advanced bidding logic.
No. You can start with a small subset of fields, including request ID, impression data, bid floor, site or app context, device details, and tmax. The full specification matters later when you handle more inventory types, exchange-specific extensions, and production requirements.
A useful starting set is id, imp, imp[].id, imp[].bidfloor, site or app, device, geo when available, and tmax. That is usually enough to identify the auction, inspect the opportunity, apply a simple strategy, and decide whether to bid.
No-bid means the bidder intentionally chooses not to participate in that auction. This is normal behavior. A bidder should return no-bid quickly when the traffic does not match targeting rules, the floor is too high, required fields are missing, or there is not enough time left to respond safely.
tmax important?tmax defines the maximum response time budget the exchange allows. It matters because RTB is latency-sensitive. If your bidder responds too slowly, the exchange may ignore the bid even if your logic was correct.
No. This is a learning prototype, not a production DSP. It can teach the request-response loop, parsing, response structure, and basic strategy design, but production systems also need pacing, budgets, creative compliance, identity handling, attribution, observability, scaling, fraud controls, and exchange-specific certification.
A good first strategy is rule-based and deterministic. For example, bid only on US mobile web traffic from an approved publisher allowlist when the bid floor is below a configured maximum CPM. That keeps the logic easy to test and improve.
Run the bidder with FastAPI and Uvicorn, then send sample OpenRTB JSON payloads with curl or Postman to a local /bid endpoint. Test both a bid scenario and a no-bid scenario so you can confirm parsing, floor checks, response formatting, and timeout handling.
At minimum, a learning example should include the request ID, a seatbid array, and a bid object with bid ID, impression ID, price, creative ID, and any demo markup or notification fields used in the example. Exact requirements vary by exchange and media type, so production integrations should follow official documentation.
Building your own bidder makes sense when you need custom experimentation, unique optimization logic, or a deeper understanding of programmatic systems. Managed infrastructure is usually the better choice when speed, operational support, compliance, and reduced engineering burden matter more than full control.
[^1]: OpenRTB behavior and field usage can vary by version and exchange implementation. Always verify against official partner documentation. [^2]: FastAPI documentation: https://fastapi.tiangolo.com/ [^3]: Uvicorn documentation: https://www.uvicorn.org/
Would you like to contribute content to this article? Contact us today!
No comments yet. Be the first to comment on this article!