Dynamic bid floors in AdMob

Setup in AdMob

Set up multiple Ad Units per Ad Format in the AdMob dashboard. one default ad unit per ad format and as many as possible additional ad units with a floor price set. These floor prices are determined and provided to you as described in the introduction.

Requesting user floor price 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.
  • Recommended MAX AdUnit Id: recommended_interstitial_ad_unit_id, recommended_rewarded_ad_unit_id andrecommended_banner_ad_unit_id
NeftaPlugin._instance.GetBehaviourInsight(
  ["recommended_rewarded_ad_unit_id", "calculated_user_floor_price_rewarded"],
  callback: { (insights: [String: Insight]) in
    _recommendedAdUnitId = nil
    _calculatedBidFloor = 0
    if let recommendedAdUnitInsight = insights[AdUnitIdInsightName] {
       _recommendedAdUnitId = recommendedAdUnitInsight._string
    }
    if let floorPriceInsight = insights[FloorPriceInsightName] {
       _calculatedBidFloor = floorPriceInsight._float
    }
}
NeftaPlugin._instance.GetBehaviourInsight(
  new String[] { "recommended_rewarded_ad_unit_id", "calculated_user_floor_price_rewarded" },
  (HashMap<String, Insight> insights) -> {
      _recommendedAdUnitId = null;
      _calculatedBidFloor = 0;
      if (insights.containsKey(AdUnitIdInsightName)) {
         _recommendedAdUnitId = insights.get(AdUnitIdInsightName)._string;
      }
      if (insights.containsKey(FloorPriceInsightName)) {
         _calculatedBidFloor = insights.get(FloorPriceInsightName)._float;
      }
   }
);
Adapter.GetBehaviourInsight(
  new string[] { "recommended_rewarded_ad_unit_id", "calculated_user_floor_price_rewarded"},
  (Dictionary<string, Insight> insights) => {
     _recommendedAdUnitId = null;
     _calculatedBidFloor = 0;
     if (insights.TryGetValue(AdUnitIdInsightName, out var insight))
     {
        _recommendedAdUnitId = insight._string;
     }
     if (insights.TryGetValue(FloorPriceInsightName, out insight))
     {
        _calculatedBidFloor = insight._float;
     }
  }
);

🚧

Validate returned values

You should checks that the returned values are valid. Only proceed with using the value if a valid response is returned.

You are guaranteed to receive the callback in the same thread with all keys that you specified in the request.

Log outcome of ad opportunity

After you have received user insights, validated the response and requested an ad in AdMob using the recommended_*_ad_unit_id field, you 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:

NeftaAdapter.onExternalMediationRequestLoad(AdType.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, rewarded: _rewarded)
@Override
public void onAdLoaded(@NonNull RewardedAd ad) {
     NeftaAdapter.OnExternalMediationRequestLoaded(NeftaAdapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, ad);
}
Adapter.OnExternalMediationRequestLoaded(Adapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, _loadedAdUnitId, ad);

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

NeftaAdapter.onExternalMediationRequestFail(AdType.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, adUnitId: adUnitId, error: error)
@Override
public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) {
    NeftaAdapter.OnExternalMediationRequestFailed(NeftaAdapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, _loadedAdUnitId, loadAdError);
}
Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, _loadedAdUnitId, error);

When an ad successfully shows log the impression with this:

func onPaid(adValue: GADAdValue) {
    NeftaAdapter.onExternalMediationImpression(.rewarded, adUnitId: _loadedAdUnitId!, adValue: adValue)
}
@Override
public void onPaidEvent(@NonNull AdValue adValue) {
    NeftaAdapter.OnExternalMediationImpression(NeftaAdapter.AdType.Rewarded, _loadedAdUnitId, adValue);
}
private void OnAdPaid(AdValue adValue)
{
    Adapter.OnExternalMediationImpression(Adapter.AdType.Rewarded, _loadedAdUnitId, adValue);
}

Initial Ad Unit has no fill

If the initially recommended ad unit doesn't return an ad:

  • Re-request the bid floor price and recommended ad unit id: the new floor price takes into account the previous no-fill event and is adjusted in real time. Then request the ad again. You can repeat this process until you get fill. You should include a delay between non filled requests as required by AdMob. As mentioned in the introduction, Nefta insures your ads are served within time windows you specify and balances fill %, revenue uplift and latency.

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([AdUnitIdInsightName, FloorPriceInsightName], callback: OnBehaviourInsight)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            if self._isLoadRequested {
                self._recommendedAdUnitId = nil
                self._calculatedBidFloor = 0
                self.Load()
            }
        }
    }
    
    func OnBehaviourInsight(insights: [String: Insight]) {
        _recommendedAdUnitId = nil
        _calculatedBidFloor = 0
        if let recommendedAdUnitInsight = insights[AdUnitIdInsightName] {
            _recommendedAdUnitId = recommendedAdUnitInsight._string
        }
        if let floorPriceInsight = insights[FloorPriceInsightName] {
            _calculatedBidFloor = floorPriceInsight._float
        }
        
        print("OnBehaviourInsight for Rewarded: \(String(describing: _recommendedAdUnitId)) calculated bid floor:\(_calculatedBidFloor)")

        if _isLoadRequested {
            Load()
        }
    }
    
    private func Load() {
        _isLoadRequested = false
        
        _loadedAdUnitId = DefaultAdUnitId
        if let recommendedAdUnitId = _recommendedAdUnitId, !recommendedAdUnitId.isEmpty {
            _loadedAdUnitId = recommendedAdUnitId
        }
        
        SetInfo("Loading Rewarded \(_loadedAdUnitId!)")
        
        let adUnitId = _loadedAdUnitId!
        Task {
            do {
                _rewarded = try await GADRewardedAd.load(withAdUnitID: adUnitId, request: GADRequest())
                _rewarded!.paidEventHandler = onPaid
                _rewarded!.fullScreenContentDelegate = self
                
                NeftaAdapter.onExternalMediationRequestLoad(AdType.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, rewarded: _rewarded)
                
                DispatchQueue.main.async {
                    self.SetInfo("Loaded Rewarded \(adUnitId)")
                    self._showButton.isEnabled = true
                }
            } catch {
                NeftaAdapter.onExternalMediationRequestFail(AdType.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, adUnitId: adUnitId, error: error)
                
                DispatchQueue.main.async {
                    self.SetInfo("Failed to load Rewarded \(adUnitId) with error: \(error.localizedDescription)")
                    
                    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                        self.GetInsightsAndLoad()
                    }
                }
            }
        }
    }
    
    func onPaid(adValue: GADAdValue) {
        NeftaAdapter.onExternalMediationImpression(.rewarded, adUnitId: _loadedAdUnitId!, adValue: adValue)
        
        SetInfo("onPaid \(adValue)")
    }
    private void GetInsightsAndLoad() {
        _isLoadRequested = true;

        NeftaPlugin._instance.GetBehaviourInsight(new String[] { AdUnitIdInsightName, FloorPriceInsightName }, this::OnBehaviourInsight);

        _handler.postDelayed(() -> {
            if (_isLoadRequested) {
                _recommendedAdUnitId = null;
                _calculatedBidFloor = 0;
                Load();
            }
        }, 5000);
    }

    private void OnBehaviourInsight(HashMap<String, Insight> insights) {
        _recommendedAdUnitId = null;
        _calculatedBidFloor = 0;
        if (insights.containsKey(AdUnitIdInsightName)) {
            _recommendedAdUnitId = insights.get(AdUnitIdInsightName)._string;
        }
        if (insights.containsKey(FloorPriceInsightName)) {
            _calculatedBidFloor = insights.get(FloorPriceInsightName)._float;
        }

        Log.i(TAG, "OnBehaviourInsights for Rewarded: "+ _recommendedAdUnitId +", calculated bid floor: "+ _calculatedBidFloor);

        if (_isLoadRequested) {
            Load();
        }
    }

    private void Load() {
        _isLoadRequested = false;

        _loadedAdUnitId = _recommendedAdUnitId != null && !_recommendedAdUnitId.isEmpty() ? _recommendedAdUnitId : DefaultAdUnitId;

        Log.i(TAG, "Loading Rewarded "+ _loadedAdUnitId);

        RewardedAd.load(_activity, _loadedAdUnitId, new AdRequest.Builder().build(),
            new RewardedAdLoadCallback() {
                @Override
                public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) {
                    NeftaAdapter.OnExternalMediationRequestFailed(NeftaAdapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, _loadedAdUnitId, loadAdError);

                    Log.i(TAG, "onAdFailedToLoad "+ loadAdError.getMessage());

                    _loadButton.setEnabled(true);

                    _handler.postDelayed(RewardedWrapper.this::GetInsightsAndLoad, 5000);
                }

                @Override
                public void onAdLoaded(@NonNull RewardedAd ad) {
                    NeftaAdapter.OnExternalMediationRequestLoaded(NeftaAdapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, ad);

                    Log.i(TAG, "onAdLoaded "+ ad.getAdUnitId());

                    _rewardedAd = ad;
                    _rewardedAd.setFullScreenContentCallback(RewardedWrapper.this);
                    _rewardedAd.setOnPaidEventListener(RewardedWrapper.this);

                    _showButton.setEnabled(true);
                }
            });
        _loadButton.setEnabled(false);
    }

    @Override
    public void onPaidEvent(@NonNull AdValue adValue) {
        NeftaAdapter.OnExternalMediationImpression(NeftaAdapter.AdType.Rewarded, _loadedAdUnitId, adValue);

        Log.i(TAG, "onPaidEvent "+ adValue);
    }
        private void GetInsightsAndLoad()
        {
            Adapter.GetBehaviourInsight(new string[] { AdUnitIdInsightName, FloorPriceInsightName }, OnBehaviourInsight);
            
            _fallbackCoroutine = StartCoroutine(LoadFallback());
        }
        
        private void OnBehaviourInsight(Dictionary<string, Insight> insights)
        {
            _recommendedAdUnitId = null;
            _calculatedBidFloor = 0;
            if (insights.TryGetValue(AdUnitIdInsightName, out var insight))
            {
                _recommendedAdUnitId = insight._string;
            }
            if (insights.TryGetValue(FloorPriceInsightName, out insight))
            {
                _calculatedBidFloor = insight._float;
            }

            Debug.Log($"OnBehaviourInsight for Rewarded: {_recommendedAdUnitId}, calculated bid floor: {_calculatedBidFloor}");
            
            if (_fallbackCoroutine != null)
            {
                Load();
            }
        }

        private void Load()
        {
            if (_fallbackCoroutine != null)
            {
                StopCoroutine(_fallbackCoroutine);
                _fallbackCoroutine = null;
            }
            
            _loadedAdUnitId = DefaultAdUnitId;
            if (!string.IsNullOrEmpty(_recommendedAdUnitId))
            {
                _loadedAdUnitId = _recommendedAdUnitId;
            }
            
            var adRequest = new AdRequest();
            RewardedAd.Load(_loadedAdUnitId, adRequest, (RewardedAd ad, LoadAdError error) =>
            {
                if (error != null)
                {
                    Adapter.OnExternalMediationRequestFailed(Adapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, _loadedAdUnitId, error);

                    SetStatus("Rewarded ad failed to load an ad with error : " + error);
                    return;
                }
                if (ad == null)
                {
                    SetStatus("Unexpected error: Rewarded load event fired with null ad and null error.");
                    return;
                }
                
                Adapter.OnExternalMediationRequestLoaded(Adapter.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, _loadedAdUnitId, ad);
                
                SetStatus($"Rewarded ad loaded with response {ad.GetResponseInfo()}", true);
                _rewardedAd = ad;
                ad.OnAdPaid += OnAdPaid;
                ad.OnAdImpressionRecorded += OnAdImpressionRecorded;
                ad.OnAdClicked += OnAdClicked;
                ad.OnAdFullScreenContentOpened += OnAdFullScreenContentOpened;
                ad.OnAdFullScreenContentClosed += OnAdFullScreenContentClosed;
                ad.OnAdFullScreenContentFailed += OnAdFullScreenContentFailed;
            });
        }
        
        private void OnAdPaid(AdValue adValue)
        {
            Adapter.OnExternalMediationImpression(Adapter.AdType.Rewarded, _loadedAdUnitId, adValue);
            
            SetStatus($"OnAdPaid {adValue}");
        }