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
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 valuesIt 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:
- 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 {
_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;
}
Updated about 1 month ago