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
.
NeftaPlugin._instance.GetBehaviourInsight(
["calculated_user_floor_price_interstitial"],
callback: { insights in
_calculatedBidFloor = 0
if let bidFloorInsight = insights[FloorPriceInsightName] {
_calculatedBidFloor = bidFloorInsight._float
}
}
}
NeftaPlugin._instance.GetBehaviourInsight(new String[] { "calculated_user_floor_price_rewarded" }, (HashMap<String, Insight> insights) -> {
_calculatedBidFloor = 0;
if (insights.containsKey(FloorPriceInsightName)) {
_calculatedBidFloor = insights.get(FloorPriceInsightName)._float;
}
));
Adapter.GetBehaviourInsight(new string[] { "calculated_user_floor_price_interstitial" }, (insights) => {
_calculatedBidFloor = 0f;
if (insights.TryGetValue(FloorPriceInsightName, out var insight)) {
_calculatedBidFloor = insight._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)
NeftaCustomAdapter.OnExternalMediationRequestLoaded(NeftaCustomAdapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, adInfo);
Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, error);
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)
NeftaCustomAdapter.OnExternalMediationRequestFailed(NeftaCustomAdapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, error);
Adapter.OnExternalMediationRequestLoaded(Adapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, info);
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
Full example:
- Native iOS: https://github.com/Nefta-io/NeftaISAdapter/blob/main/ISIntegration/Rewarded.swift
- Native Android: https://github.com/Nefta-io/NeftaISAdapter-Android/blob/main/ISIntegration/src/main/java/com/nefta/is/RewardedWrapper.java
- Unity: https://github.com/Nefta-io/NeftaISAdapter-Unity/blob/main/Assets/AdDemo/RewardedController.cs
Below is an example showing the integration of dynamic floors in LevelPlay (not production-ready) .
private func GetInsightsAndLoad() {
_isLoadRequested = true
NeftaPlugin._instance.GetBehaviourInsight([FloorPriceInsightName], callback: OnBehaviourInsight)
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
if self._isLoadRequested {
self._calculatedBidFloor = 0
self.Load()
}
}
}
func OnBehaviourInsight(insights: [String: Insight]) {
_calculatedBidFloor = 0
if let bidFloorInsight = insights[FloorPriceInsightName] {
_calculatedBidFloor = bidFloorInsight._float
}
print("OnBehaviourInsight for Rewarded calculated bid floor: \(_calculatedBidFloor)")
if _isLoadRequested {
Load()
}
}
func Load() {
_isLoadRequested = false
if _calculatedBidFloor == 0 {
_bidFloor = 0
IronSource.setWaterfallConfiguration(ISWaterfallConfiguration.clear(), for: ISAdUnit.is_AD_UNIT_REWARDED_VIDEO())
} else {
_bidFloor = _calculatedBidFloor
let configuration = ISWaterfallConfiguration.builder()
.setFloor(NSNumber(value: _bidFloor))
.build()
IronSource.setWaterfallConfiguration(configuration, for: ISAdUnit.is_AD_UNIT_REWARDED_VIDEO())
}
SetInfo("Loading Rewarded with floor: \(_bidFloor)")
_rewarded = LPMRewardedAd(adUnitId: "doucurq8qtlnuz7p")
_rewarded.setDelegate(self)
_rewarded.loadAd()
}
func didFailToLoadAd(withAdUnitId adUnitId: String, error: any Error) {
ISNeftaCustomAdapter.onExternalMediationRequestFail(.rewarded, requestedFloorPrice: _bidFloor, calculatedFloorPrice: _calculatedBidFloor, adUnitId: adUnitId, error: error as NSError)
SetInfo("didFailToLoadAd \(adUnitId): \(error.localizedDescription)")
// or automatically retry with a delay
// DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
// self.GetInsightsAndLoad()
// }
}
func didLoadAd(with adInfo: LPMAdInfo) {
ISNeftaCustomAdapter.onExternalMediationRequestLoad(.rewarded, requestedFloorPrice: _bidFloor, calculatedFloorPrice: _calculatedBidFloor, adInfo: adInfo)
SetInfo("didLoadAd \(adInfo)")
_showButton.isEnabled = true
}
private void GetInsightsAndLoad() {
_isLoadRequested = true;
NeftaPlugin._instance.GetBehaviourInsight(new String[] { FloorPriceInsightName }, this::OnBehaviourInsight);
_handler.postDelayed(() -> {
if (_isLoadRequested) {
_calculatedBidFloor = 0;
Load();
}
}, 5000);
}
private void OnBehaviourInsight(HashMap<String, Insight> insights) {
_calculatedBidFloor = 0;
if (insights.containsKey(FloorPriceInsightName)) {
_calculatedBidFloor = insights.get(FloorPriceInsightName)._float;
}
Log("OnBehaviourInsights for Rewarded calculated bid floor: "+ _calculatedBidFloor);
if (_isLoadRequested) {
Load();
}
}
private void Load() {
_isLoadRequested = false;
if (_calculatedBidFloor <= 0) {
_bidFloor = 0;
IronSource.setWaterfallConfiguration(WaterfallConfiguration.empty(), IronSource.AD_UNIT.REWARDED_VIDEO);
} else {
_bidFloor = _calculatedBidFloor;
WaterfallConfiguration.WaterfallConfigurationBuilder builder = WaterfallConfiguration.builder();
WaterfallConfiguration waterfallConfiguration = builder
.setFloor(_bidFloor)
.build();
IronSource.setWaterfallConfiguration(waterfallConfiguration, IronSource.AD_UNIT.REWARDED_VIDEO);
}
Log("Loading Rewarded with floor: "+ _bidFloor);
_rewarded = new LevelPlayRewardedAd("kftiv52431x91zuk");
_rewarded.setListener(RewardedWrapper.this);
_rewarded.loadAd();
}
@Override
public void onAdLoadFailed(@NonNull LevelPlayAdError error) {
NeftaCustomAdapter.OnExternalMediationRequestFailed(NeftaCustomAdapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, error);
Log("onAdLoadFailed: "+ error);
_loadButton.setEnabled(true);
_showButton.setEnabled(false);
// or automatically retry with a delay
//_handler.postDelayed(this::GetInsightsAndLoad, 5000);
}
@Override
public void onAdLoaded(@NonNull LevelPlayAdInfo adInfo) {
NeftaCustomAdapter.OnExternalMediationRequestLoaded(NeftaCustomAdapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, adInfo);
Log("onAdLoaded " + adInfo);
_showButton.setEnabled(true);
}
private void GetInsightsAndLoad()
{
_isLoadRequested = true;
Adapter.GetBehaviourInsight(new string[] { FloorPriceInsightName }, OnBehaviourInsight);
StartCoroutine(LoadFallback());
}
private void OnBehaviourInsight(Dictionary<string, Insight> insights)
{
_calculatedBidFloor = 0f;
if (insights.TryGetValue(FloorPriceInsightName, out var insight)) {
_calculatedBidFloor = insight._float;
}
Debug.Log($"OnBehaviourInsight for Rewarded calculated bid floor: {_calculatedBidFloor}");
if (_isLoadRequested)
{
Load();
}
}
private void Load()
{
_isLoadRequested = false;
if (_calculatedBidFloor == 0)
{
IronSource.Agent.SetWaterfallConfiguration(WaterfallConfiguration.Empty(), AdFormat.RewardedVideo);
}
else
{
var configuration = WaterfallConfiguration.Builder()
.SetFloor(_bidFloor)
.SetCeiling(_bidFloor + 200) // when using SetFloor, SetCeiling has to be used as well
.Build();
IronSource.Agent.SetWaterfallConfiguration(configuration, AdFormat.RewardedVideo);
}
_rewarded = new LevelPlayRewardedAd(AdUnitId);
_rewarded.OnAdLoaded += OnAdLoaded;
_rewarded.OnAdLoadFailed += OnAdLoadFailed;
_rewarded.OnAdDisplayed += OnAdDisplayed;
_rewarded.OnAdDisplayFailed += OnAdDisplayFailed;
_rewarded.OnAdRewarded += OnAdRewarded;
_rewarded.OnAdClicked += OnAdClicked;
_rewarded.OnAdInfoChanged += OnAdInfoChanged;
_rewarded.OnAdClosed += OnAdClosed;
_rewarded.LoadAd();
SetStatus($"Loading Rewarded calculatedFloor: {_calculatedBidFloor}");
}
private void OnAdLoadFailed(LevelPlayAdError error)
{
Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, error);
SetStatus($"OnAdLoadFailed {error}");
// or automatically retry with a delay
//StartCoroutine(ReTryLoad());
}
private void OnAdLoaded(LevelPlayAdInfo info)
{
Adapter.OnExternalMediationRequestLoaded(Adapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, info);
SetStatus($"OnAdLoaded {info}");
_show.interactable = true;
}
Updated about 5 hours ago