Dynamic bid floors in MAX
Since the transition to real time bidding, ad monetisation managers have lost control and visibility in their waterfalls. Setting floor prices is the mechanism to optimise bidding networks and deliver ad revenue uplift to partners. Nefta has seen 10-20% ad revenue increase with a comprehensive use of bid floor prices. In order to effectively set floor prices, partners need advanced user value estimation models and market price or bid landscape models.
Setup in MAX
Partners should set up multiple Ad Units per Ad Format in the MAX dashboard. It's suggested to have one default ad unit per ad format and numerous additional ad units with a floor price set. The floor price will be set at different bucket intervals, for example: 1, 5, 10, 20, 50, 100, 500
.
The right number of ad units
In general the more ad units with a floor price bucket per ad format, the better. If you have a limit of ad units per ad format, its suggested to talk to your Applovin account representative to increase. If this is not possible, start with as many as possible and optimise from there. Discuss with your Nefta account representative.
Setting the right floor prices
To get started, its suggested to use a best guess approach of value buckets for your users. Alternatively, discuss with your Nefta account representative to provide value buckets based on the ad data available.
Optimising floor buckets
- Using data to understand which floor price buckets are delivering the largest fill rate and splitting the floor bucket into new price buckets.
- Analysing predicted user value in the default ad unit to discover additional untapped value buckets to add.
Receive the dynamic bid floor for the user from Nefta
Firstly, integrate user insights and request the following user insight values:
- Predicted floor price per user per ad format:
calculated_user_floor_price_banner
,calculated_user_floor_price_interstitial
,calculated_user_floor_price_rewarded
.
Select the right initial Ad Unit
Using the calculated_user_floor_price_*
value as requested above, select the Ad unit with the closest bid floor price and call the function MaxSdk.LoadRewardedAd(adUnitId);
in the case of a rewarded ad unit.
Ad unit selection logic:
- Partners can select the closest ad unit with a floor price thats lower than
calculated_user_floor_price_*
, this will result in a higher chance of fill but less optimisation. - Partners can select the closest ad unit with a percentage difference, for example above/bellow 5%.
It's important to continuously optimise the ad unit selection, number of ad units and price buckets.
Initial Ad Unit has no fill
If the initially selected ad unit doesn't return an ad, either you can select an ad unit with a lower price bucket or select your default ad unit.
Example code
Below is example code (not production-ready) showing the potential integration of dynamic floors.
struct PlacementConfig {
var index: Int
var placementId: String
var cpm: Double
}
let _placements: [PlacementConfig] = [
// Add as many placements as you like.
PlacementConfig(index: 0, placementId: "b5231.....eb4dv", cpm: 0),
PlacementConfig(index: 1, placementId: "a8431.....8eb4d1", cpm: 25.0),
PlacementConfig(index: 2, placementId: "34682.....e9b052", cpm: 50.0),
PlacementConfig(index: 3, placementId: "d066e.....d29f8b", cpm: 75.0),
]
var _selectedPlacement: PlacementConfig?
var _calculatedFloorPrice: Float64 = 0
@objc func Show() {
if _selectedPlacement == nil {
_selectedPlacement = _placements.first
if let insights = _insights {
_calculatedFloorPrice = insights["calculated_user_floor_price_banner"]?._float ?? 0
for placment in _placements {
if placment.cpm > bid_floor_price {
break
// Could add a 5/10% flexibility if its up or down. Make sense to push higher
}
_selectedPlacement = placment
// By logging the difference between placement price and bid_floor_price, partners can understand the value being left on the table which can be optimised by adding more placements.
}
}
}
_adView = MAAdView(adUnitIdentifier: _selectedPlacement!.placementId)
_adView.delegate = self
_adView.frame = CGRect(x: 0, y: 0, width: 320, height: 50)
_bannerPlaceholder.addSubview(_adView)
_adView.loadAd()
}
func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
ALNeftaMediationAdapterSwift.OnExternalAdFail(.Banner, unitFloorPrice: _selectedPlacement!.cpm, calculatedFloorPrice: _calculatedFloorPrice, error: error)
if error.code == .noFill {
if _selectedPlacement!.index > 0 {
_selectedPlacement = _placements[_selectedPlacement!.index - 1]
// Stategies:
// 1. Simply continue going down until a placement delivers fill
// 2. Select the closest Adplacement ± 5%, select the 2nd best, then go to defualt placement.
// Alternatively, switch to default placement.
}
}
}
func didLoad(_ ad: MAAd) {
ALNeftaMediationAdapterSwift.OnExternalAdLoad(.Banner, unitFloorPrice: _selectedPlacement!.cpm, calculatedFloorPrice: _calculatedFloorPrice)
// Try to increase bid floor
// if _selectedPlacement!.index < _placements.count - 1 {
// _selectedPlacement = _placements[_selectedPlacement!.index + 1]
// }
}
}
Updated 3 days ago