This is a simulator for a Demand-Side Platform (DSP) and Supply-Side Platform (SSP), designed to demonstrate:
- Real-time bidding (RTB) workflow – request, bid, feedback loops.
- Bit-flag audience targeting for fast campaign matching.
- Concurrent, in-memory stores, easily swappable with Redis or SQL
- Console simulation or standalone web APIs
sequenceDiagram
autonumber
loop every 500ms
participant SSP
participant Alpha_DSP
participant Beta_DSP
participant Gamma_DSP
SSP->>Alpha_DSP: POST /bid (AvailableBidRequest)
Alpha_DSP->>Alpha_DSP: Evaluate all campaigns & pick best
Alpha_DSP-->>SSP: BidDecision (bidId, bidAmt)
SSP->>Beta_DSP: POST /bid (AvailableBidRequest)
Beta_DSP->>Beta_DSP: Evaluate all campaigns & pick best
Beta_DSP-->>SSP: BidDecision (bidId, bidAmt)
SSP->>Gamma_DSP: POST /bid (AvailableBidRequest)
Gamma_DSP->>Gamma_DSP: Evaluate all campaigns & pick best
Gamma_DSP-->>SSP: BidDecision (bidId, bidAmt)
SSP->>SSP: Select highest bid across DSPs (≤200ms)
SSP-->>Alpha_DSP: POST /feedback (Win/Loss)
SSP-->>Beta_DSP: POST /feedback (Win/Loss)
SSP-->>Gamma_DSP: POST /feedback (Win/Loss)
end
- SSP emits a
BidRequest
every 500 ms and waits 200 ms for responses. - Each DSP evaluates all eligible campaigns and responds with a BidDecision.
- The SSP selects the highest bid and notifies all DSPs of the outcome.
- Only the winner’s campaign budget is finally debited.
Path | What lives here |
---|---|
Shared/ | Shared models (BidRequest , CampaignDetail , …) and JSON loaders |
DSP.Api/ | Minimal web API exposing bid, campaign & user endpoints |
SSP.Api/ | SSP event source + feedback fan-out |
Simulation/ | Console simulation wiring three DSPs to one SSP |
*.json | Synthetic users and campaign datas |
# 1. Prerequisites
dotnet --version # Should be 9.0.x
# 2. Build all projects
dotnet build mini-dsp.sln
# 3. Launch the simulation
cd Simulation
dotnet run
You should see logs such as:
[DSP Alpha] Received BidRequest for user-123
[DSP Alpha] Bidding 2.55 for campaign 93f…
[DSP Alpha] Feedback: Bid 3e7… – WON
Hit Enter to stop the simulation loop.
cd DSP.Api
dotnet run # http://localhost:5294 by default
Method | Path | Purpose |
---|---|---|
POST |
/users |
Create/update a user r |
GET |
/users/{id} |
Retrieve user by ID |
POST |
/campaigns |
Register a new campaign |
GET |
/campaigns/{id} |
Retrieve campaign by ID |
PATCH |
/campaigns/{id}/budget?newBudget=X |
Update campaign budget cap |
POST |
/bid |
Evaluate incoming bid request and respond |
POST |
/feedback |
Send feedback (win/loss) and manage refunds |
The SSP is instantiated programmatically in Simulation/Program.cs
.
To run it independently, wrap Ssp in an HTTP or gRPC host – the logic is framework-agnostic.
File | Records | Notes |
---|---|---|
Shared/users.json |
30 | Randomised demographic + location flags generated by generate_users.py |
Shared/campaign_*.json |
5 | Five campaigns with densely populated bidLines generated by generate_campaigns.py |
Loaders live in Shared/Utilities
and return strongly typed lists.
Every campaign stores:
Field | Type | Meaning |
---|---|---|
budgetCap |
decimal | Total budget the advertiser has authorised |
BudgetSpent |
decimal | Finalised spend & in-flight requestt |
RemainingBudget |
decimal | Budget left in the campaign |
The remaining budget is therefore:
decimal RemainingBudget => Math.Max(BudgetCap - BudgetSpent, 0);
-
Optimistic Deduction:
When a DSP places a bid, it when the DSP sends a bid it adds the bid amount toBudgetSpent
immediately. -
Win Confirmation:
If the bid wins, the spent bid becomes final. -
Loss Refund:
If the bid loses, the DSP refunds the deducted amount immediately.
This prevents overdrafts even under high concurrency because the worst-case remaining budget seen by the bidding loop is under-estimated, never over-estimated.
Note: We're using a 1PA (First-Price Auction) model.
This means the winning DSP pays exactly what it bids — unlike traditional RTB setups that use 2PA (Second-Price Auction), where the winner pays slightly more than the second-highest bid.In 1PA, bid shading strategies are often used to avoid overpaying, but for now we use direct
BaseBid * BidFactor
pricing for clarity.
This also makes budget accounting more deterministic and testable.
sequenceDiagram
participant SSP
participant DSP_A as DSP A (Bids 5.00)
participant DSP_B as DSP B (Bids 4.50)
participant DSP_C as DSP C (Bids 3.75)
SSP->>DSP_A: You win the auction!
Note right of DSP_A: 1PA: Pays 5.00<br>2PA: Would pay 4.51
var userFlags = TargetingExtensions.ToFlags(user.TargetingData);
var bidFlags = TargetingExtensions.ToFlags(bidLine.TargetingData);
bool match = (userFlags & bidFlags) == bidFlags;
A perfect superset match is required. Partial overlaps do not qualify – all bidLine criteria must be met.
- Bit-flags give constant-time checks.
- Densely populated
bidLines
(think “all combinations of age, gender, city, interests”) mean almost any qualified user has a matching line – no fuzzy scoring pass required.
decimal bid = campaign.BaseBid * bidLine.BidFactor;
Campaigns have an optional flag:
If no bidLine
matches and forAwareness == true
, the DSP will place a default bid = baseBid.
Great for brand-lift experiments or launching in a new market where user data is spotty.
Otherwise, the DSP would not bid for that campaign.
Two campaigns inside one DSP may sometimes generate identical bid amounts.
Instead of always picking the first match, use a round-robin selection among all campaigns that tie at the highest bid.
- For every tied group of campaigns bidding X, it keeps a rotating pointer (ring buffer).
- Each time a bid comes in and X is the max bid, the next campaign in line is chosen.
- This avoids "first-in-list" bias and spreads impressions evenly among tied bids.