Bid floors in LevelPlay

Setup in LevelPlay

LevelPlay enables partners to dynamically set bid floor prices using the "Price limitations using waterfall configuration". For every ad opportunity, you first obtain a floor price from Nefta, then create aWaterfallConfiguration with the floor parameter set and finally request an ad. If no ad is returned, you adhere to the provider specified delay and then repeat the process, by first obtaining a new floor price (which is adapted in real time and takes the previous no fill into consideration) and then requesting another ad as before.

Request user insights

Via the SDK, partners will request user specific floor price insight key-values:

  • User value bid floor price: , calculated_user_floor_price_interstitial, calculated_user_floor_price_rewarded and calculated_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:

func didLoadAd(with adInfo: LPMAdInfo) {
    ISNeftaCustomAdapter.onExternalMediationRequestLoad(.rewarded, requestedFloorPrice: _calculatedBidFloor, calculatedFloorPrice: _calculatedBidFloor, adInfo: adInfo)
}
@Override
public void onAdLoaded(@NonNull LevelPlayAdInfo adInfo) {
    NeftaCustomAdapter.OnExternalMediationRequestLoaded(NeftaCustomAdapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, adInfo);
}
private void OnAdLoaded(LevelPlayAdInfo info)
{
    Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, error);
}

When the ad fails to loads, log the response using the following function:

func didFailToLoadAd(withAdUnitId adUnitId: String, error: any Error) {
    ISNeftaCustomAdapter.onExternalMediationRequestFail(.rewarded, requestedFloorPrice: _calculatedBidFloor, calculatedFloorPrice: _calculatedBidFloor, adUnitId: adUnitId, error: error as NSError)
}
@Override
public void onAdLoadFailed(@NonNull LevelPlayAdError error) {
    NeftaCustomAdapter.OnExternalMediationRequestFailed(NeftaCustomAdapter.AdType.Rewarded, _bidFloor, _calculatedBidFloor, error);
}
private void OnAdLoadFailed(LevelPlayAdError 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:

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 {
            _requestedBidFloor = 0
            IronSource.setWaterfallConfiguration(ISWaterfallConfiguration.clear(), for: ISAdUnit.is_AD_UNIT_REWARDED_VIDEO())
        } else {
            _requestedBidFloor = _calculatedBidFloor
            let configuration = ISWaterfallConfiguration.builder()
                .setFloor(NSNumber(value: _requestedBidFloor))
                .build()
            IronSource.setWaterfallConfiguration(configuration, for: ISAdUnit.is_AD_UNIT_REWARDED_VIDEO())
        }
        
        SetInfo("Loading Rewarded with floor: \(_requestedBidFloor)")
        
        _rewarded = LPMRewardedAd(adUnitId: "doucurq8qtlnuz7p")
        _rewarded.setDelegate(self)
        _rewarded.loadAd()
    }
    
    func didFailToLoadAd(withAdUnitId adUnitId: String, error: any Error) {
        ISNeftaCustomAdapter.onExternalMediationRequestFail(.rewarded, requestedFloorPrice: _requestedBidFloor, calculatedFloorPrice: _calculatedBidFloor, adUnitId: adUnitId, error: error as NSError)
        
        SetInfo("didFailToLoadAd \(adUnitId): \(error.localizedDescription)")
      
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            self.GetInsightsAndLoad()
        }
    }
    
    func didLoadAd(with adInfo: LPMAdInfo) {
        ISNeftaCustomAdapter.onExternalMediationRequestLoad(.rewarded, requestedFloorPrice: _requestedBidFloor, 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) {
            _requestedBidFloor = 0;
            IronSource.setWaterfallConfiguration(WaterfallConfiguration.empty(), IronSource.AD_UNIT.REWARDED_VIDEO);
        } else {
            _requestedBidFloor = _calculatedBidFloor;
            WaterfallConfiguration.WaterfallConfigurationBuilder builder = WaterfallConfiguration.builder();
            WaterfallConfiguration waterfallConfiguration = builder
                    .setFloor(_requestedBidFloor)
                    .build();
            IronSource.setWaterfallConfiguration(waterfallConfiguration, IronSource.AD_UNIT.REWARDED_VIDEO);
        }

        Log("Loading Rewarded with floor: "+ _requestedBidFloor);

        _rewarded = new LevelPlayRewardedAd("kftiv52431x91zuk");
        _rewarded.setListener(RewardedWrapper.this);
        _rewarded.loadAd();
    }

    @Override
    public void onAdLoadFailed(@NonNull LevelPlayAdError error) {
        NeftaCustomAdapter.OnExternalMediationRequestFailed(NeftaCustomAdapter.AdType.Rewarded, _requestedBidFloor, _calculatedBidFloor, error);

        Log("onAdLoadFailed: "+ error);

        _loadButton.setEnabled(true);
        _showButton.setEnabled(false);
      
        _handler.postDelayed(this::GetInsightsAndLoad, 5000);
    }

    @Override
    public void onAdLoaded(@NonNull LevelPlayAdInfo adInfo) {
        NeftaCustomAdapter.OnExternalMediationRequestLoaded(NeftaCustomAdapter.AdType.Rewarded, _requestedBidFloor, _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)
            {
                _requestedBidFloor = 0;
                IronSource.Agent.SetWaterfallConfiguration(WaterfallConfiguration.Empty(), AdFormat.RewardedVideo);
            }
            else
            {
                _requestedBidFloor = _caculatedBidFloor;
                var configuration = WaterfallConfiguration.Builder()
                    .SetFloor(_requestedBidFloor)
                    .SetCeiling(_requestedBidFloor + 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: {_requestedBidFloor}");
        }
        
        private void OnAdLoadFailed(LevelPlayAdError error)
        {
            Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Rewarded, _requestedBidFloor, _calculatedBidFloor, error);
            
            SetStatus($"OnAdLoadFailed {error}");
            
            StartCoroutine(ReTryLoad());
        }
        
        private void OnAdLoaded(LevelPlayAdInfo info)
        {
            Adapter.OnExternalMediationRequestLoaded(Adapter.AdType.Rewarded, _requestedBidFloor, _calculatedBidFloor, info);
            
            SetStatus($"OnAdLoaded {info}");
            _show.interactable = true;
        }