Dynamic bid floors in LevelPlay
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.
Setup in LevelPlay
LevelPlay enables partners to dynamically set bid floor prices using the "Price limitations using waterfall configuration". For every ad opportunity, partners should create WaterfallConfiguration
with the floor
parameter set. Request an ad with the floor price set, if no ad is returned, clear the waterfall configuration and request an ad without a floor price set.
Setup in Nefta
In order to activate user-level dynamic bid floor optimisation in Nefta, partners need to request user insights and log the outcome of each ad opportunity.
Request user insights
Via the SDK, partners will request specific user insight key-values. All possible user insight keys can be found here. The most important keys for dynamic floors in LevelPlay are the following:
- User value bid floor price: ,
calculated_user_floor_price_interstitial
,calculated_user_floor_price_rewarded
andcalculated_user_floor_price_banner
.
_plugin.GetBehaviourInsight([
"calculated_user_floor_price_banner",
"calculated_user_floor_price_interstitial",
"calculated_user_floor_price_rewarded"
])
NeftaAdapterEvents.BehaviourInsightCallback = (Dictionary<string, Insight> insights) => {
let calculatedBidFloor: Double = insights["calculated_user_floor_price_interstitial"]?._float ?? 0.0
}
Adapter.GetBehaviourInsight(new string[] {
"calculated_user_floor_price_banner",
"calculated_user_floor_price_interstitial",
"calculated_user_floor_price_rewarded"
});
Adapter.BehaviourInsightCallback = (insights) => {
double calculatedBidFloor = insights["calculated_user_floor_price_banner"]._float;
}
Validate returned values
It is crucial a partner checks the values received are valid. Only if a valid response is received, a partner should proceed with using the values.
You are guaranteed to receive the callback in the same thread with all keys that you specified in the request.
Using the calculated_user_floor_price_*
calculated above, set the WaterfallConfiguration
floor
parameter to the bid floor price.
Log outcome of ad opportunity
After a partner has received user insights, validated the response and requested an ad from LevelPlay using the calculated_user_floor_price_*
field, the partner should log the outcome of the ad opportunity in order to continuously maximise ad revenue uplift.
When the ad successfully loads, log the response using the following function:
ISNeftaCustomAdapter.onExternalMediationRequestLoad(.rewarded, requestedFloorPrice: _calculatedBidFloor, calculatedFloorPrice: _calculatedBidFloor, adInfo: adInfo)
Adapter.OnExternalMediationRequestLoaded(Adapter.AdType.Interstitial, _calculatedBidFloor, _calculatedBidFloor, info);
When the ad fails to loads, log the response using the following function:
ISNeftaCustomAdapter.onExternalMediationRequestFail(.rewarded, requestedFloorPrice: _calculatedBidFloor, calculatedFloorPrice: _calculatedBidFloor, adUnitId: adUnitId, error: error as NSError)
Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Interstitial, _calculatedBidFloor, _calculatedBidFloor, error);
Initial bid floor price has no fill
If the initially selected ad unit doesn't return an ad:
- Re-request the bid floor price: the new floor price takes into account the previous no-fill event and is adjusted in real time. You can repeat this process for each ad opportunity. However, latency should be considered if requesting multiple times per ad opportunity. Nefta will help in balancing fill %, revenue uplift and latency.
- Request an ad from LevelPlay without a floor price set.
Example code
Below is an example showing the integration of dynamic floors in LevelPlay (not production-ready) .
private bool IsNoFill(int errorCode)
{
return errorCode == 509 || errorCode == 606 || errorCode == 706 || errorCode == 1058 || errorCode == 1158;
}
private double _bidFloor;
private double _calculatedBidFloor;
private int _bidNoFillCount;
public void SetInsights(Dictionary<string, Insight> insights)
{
_calculatedBidFloor = insights["calculated_user_floor_price_banner"]._float;
_bidFloor = _calculatedBidFloor;
var configuration = WaterfallConfiguration.Builder()
.SetFloor(_bidFloor)
.SetCeiling(_bidFloor * 1.5f) // high number just because it has to be set
.Build();
IronSource.Agent.SetWaterfallConfiguration(configuration, AdFormat.Banner);
}
private void LoadAd()
{
_banner = new LevelPlayBannerAd("vpkt7...yfwr4", LevelPlayAdSize.BANNER, LevelPlayBannerPosition.TopCenter, null, true, true);
_banner.OnAdLoaded += OnAdLoaded;
_banner.OnAdLoadFailed += OnAdLoadFailed;
_banner.LoadAd();
}
private void OnAdLoadFailed(LevelPlayAdError error)
{
Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Banner, _bidFloor, _calculatedBidFloor, error);
if _bidFloor > 0)
{
_bidNoFillCount++;
if (_bidNoFillCount == 1)
{
_bidFloor *= 0.95; // This is a 5% reduction for the 2nd call if first call for ad delivers no fill. this value can/should change over time.
var configuration = WaterfallConfiguration.Builder()
.SetCeiling(_bidFloor * 1.5)
.SetFloor(_bidFloor)
.Build();
IronSource.Agent.SetWaterfallConfiguration(configuration, AdFormat.Banner);
LoadAd();
}
else if (_bidNoFillCount == 2)
{
_bidFloor = 0;
IronSource.Agent.SetWaterfallConfiguration(WaterfallConfiguration.Empty(), AdFormat.Banner);
LoadAd();
}
}
}
private void OnAdLoaded(LevelPlayAdInfo adInfo)
{
Adapter.OnExternalMediationRequestLoaded(Adapter.AdType.Banner, _bidFloor, _calculatedBidFloor, adInfo);
_bidNoFillCount = 0
}
Updated 9 days ago