Dynamic bid floors in MAX

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 waterfalls and deliver ad revenue uplift to partners.

Setup in MAX

Partners should set up multiple Ad Units per Ad Format in the MAX dashboard. It's suggested to have one default ad unit per ad format and as many as possible additional ad units with a floor price set. The floor price will be set at different bucket intervals. Nefta will provide the price buckets based on eCPM and revenue distributions.

Nefta will support the partner to optimise the price buckets by analysing fill and revenue contribution.

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 MAX are the following:

  • 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
_plugin.GetBehaviourInsight([
  "calculated_user_floor_price_banner",
  "recommended_interstitial_ad_unit_id", "calculated_user_floor_price_interstitial",
  "recommended_rewarded_ad_unit_id", "calculated_user_floor_price_rewarded"
])

_plugin.OnBehaviourInsight = { (insights: [String: Insight]) in
  let recommendedAdUnitId: String? = insights["recommended_rewarded_ad_unit_id"]?._string ?? _defaultAdUnitId
  let calculatedBidFloor: Double = insights["calculated_user_floor_price_rewarded"]?._float ?? 0.0
}
NeftaAdapterEvents.GetBehaviourInsight(new string[] {
  "calculated_user_floor_price_banner",
  "recommended_interstitial_ad_unit_id", "calculated_user_floor_price_interstitial",
  "recommended_rewarded_ad_unit_id", "calculated_user_floor_price_rewarded"
});

NeftaAdapterEvents.BehaviourInsightCallback = (Dictionary<string, Insight> insights) => {
  string recommendedAdUnitId = insights["recommended_rewarded_ad_unit_id"]._string ?? _defaultAdUnitId;
  double calculatedBidFloor = insights["calculated_user_floor_price_interstitial"]?._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.

Log outcome of ad opportunity

After a partner has received user insights, validated the response and requested an ad in MAX using the recommended_*_ad_unit_id 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:

ALNeftaMediationAdapter.onExternalMediationRequestLoad(.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, ad: ad)
NeftaAdapterEvents.OnExternalMediationRequestLoaded(NeftaAdapterEvents.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, adInfo);
          

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

ALNeftaMediationAdapter.onExternalMediationRequestFail(.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, adUnitIdentifier: adUnitIdentifier, error: error)
NeftaAdapterEvents.OnExternalMediationRequestFailed(NeftaAdapterEvents.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, adUnitId, errorInfo);

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. 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 MAX using an AdUnit without a floor price - a default ad unit.

Example code

Below is example code (not production-ready) showing the integration of dynamic floors in MAX.

// Wrapper for Ads with bid floor selection logic  
class Rewarded : NSObject, MARewardedAdDelegate {
    
    private let _defaultAdUnitId = "918ac...9c034"
    
    static let InsightAdUnitId = "recommended_rewarded_ad_unit_id"
    static let InsightFloorPrice = "calculated_user_floor_price_rewarded"
    
    private var RequestNewInsights: () -> Void
    private var _selectedAdUnitId: String?
    private var _recommendedAdUnitId: String?
    private var _calculatedBidFloor: Double = 0.0
    private var _consecutiveAdFail = 0
    private var _isLoadPending = false
    
    private let _loadButton: UIButton
    private let _showButton: UIButton
    private let _status: UILabel
    
    var _rewarded: MARewardedAd!
    
    func OnBehaviourInsight(insights: [String: Insight]) {
        _recommendedAdUnitId = insights[Rewarded.InsightAdUnitId]?._string
        _calculatedBidFloor = insights[Rewarded.InsightFloorPrice]?._float ?? 0.0
        
        print("OnBehaviourInsight for Rewarded recommended AdUnit: \(String(describing: _recommendedAdUnitId))/cpm:\(_calculatedBidFloor)")
        
        _selectedAdUnitId = _recommendedAdUnitId

        if _isLoadPending {
            Load()
        }
    }

    func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
        ALNeftaMediationAdapter.onExternalMediationRequestFail(.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, adUnitIdentifier: adUnitIdentifier, error: error)
        
        if error.code == .noFill {
            _consecutiveAdFail += 1
            if _consecutiveAdFail == 1 { // in case of first no fill, try to get new insight (will probably return adUnit with lower bid floor
                _isLoadPending = true
                RequestNewInsights()
            } else { // for consequential no fills go with default (no bid floor) ad unit
                _selectedAdUnitId = nil
                Load()
            }
        }
        
        SetInfo("didFailToLoadAd \(adUnitIdentifier): \(error)")
    }
    
    func didLoad(_ ad: MAAd) {
        ALNeftaMediationAdapter.onExternalMediationRequestLoad(.rewarded, recommendedAdUnitId: _recommendedAdUnitId, calculatedFloorPrice: _calculatedBidFloor, ad: ad)
        
        _consecutiveAdFail = 0
        // Optionally request new insights on ad load, in case ad unit with higher bid floor gets recommended
        // SelectAdUnitFromInsights()
        
        SetInfo("didLoad \(ad)")
        _showButton.isEnabled = true
    }
    
    init(requestNewInsights: @escaping (() -> Void), loadButton: UIButton, showButton: UIButton, status: UILabel) {
        RequestNewInsights = requestNewInsights
        _loadButton = loadButton
        _showButton = showButton
        _status = status
        
        super.init()
        
        _loadButton.addTarget(self, action: #selector(Load), for: .touchUpInside)
        _showButton.addTarget(self, action: #selector(Show), for: .touchUpInside)
        
        _showButton.isEnabled = false
    }
    
    @objc func Load() {
        _rewarded = MARewardedAd.shared(withAdUnitIdentifier: _selectedAdUnitId ?? _defaultAdUnitId)
        _rewarded.delegate = self
        _rewarded.load()
    }
    
    @objc func Show() {
        _rewarded.show()
        
        _showButton.isEnabled = false
    }

    func didDisplay(_ ad: MAAd) {
        SetInfo("didDisplay \(ad)")
        ALNeftaMediationAdapter.onExternalMediationImpression(ad)
    }
}

// Main application entry point:
    override func viewDidLoad() {
        super.viewDidLoad()

        NeftaPlugin.EnableLogging(enable: true)        
        _plugin = NeftaPlugin.Init(appId: "5661184053215232")
        _plugin.OnBehaviourInsight = OnBehaviourInsight
        GetBehaviourInsights()

        _title.text = "Nefta Adapter for MAX"
        _banner = Banner(requestNewInsights: GetBehaviourInsights,
                         showButton: _showBanner, hideButton: _hideBanner, status: _bannerStatus, bannerPlaceholder: _bannerPlaceholder)
        _interstitial = Interstitial(requestNewInsights: GetBehaviourInsights,
                                     loadButton: _loadInterstitial, showButton: _showInterstitial, status: _interstitialStatus)
        _rewardedVideo = Rewarded(requestNewInsights: GetBehaviourInsights,
                                  loadButton: _loadRewarded, showButton: _showRewarded, status: _rewardedStatus)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.checkTrackingAndInitializeMax()
        }
    }
    
    private func GetBehaviourInsights() {
        _plugin.GetBehaviourInsight([
            Banner.InsightFloorPrice,
            Interstitial.InsightAdUnitId, Interstitial.InsightFloorPrice,
            Rewarded.InsightAdUnitId, Rewarded.InsightFloorPrice
        ])
    }
    
    private func OnBehaviourInsight(insights: [String: Insight]) {
        _banner.OnBehaviourInsight(insights: insights)
        _interstitial.OnBehaviourInsight(insights: insights)
        _rewardedVideo.OnBehaviourInsight(insights: insights)
    }
    
		// the rest of your class
}


    // Wrapper for Ads with bid floor selection logic    
    public class Rewarded
    {
#if UNITY_IOS
        private const string _defaultAdUnitId = "08304643cb16df3b";
#else // UNITY_ANDROID
        private const string _defaultAdUnitId = "3082ee9199cf59f0";
#endif
        
        public static readonly string AdUnitId = "recommended_rewarded_ad_unit_id";
        public static readonly string FloorPrice = "calculated_user_floor_price_rewarded";

        private Action GetInsights;
        private string _selectedAdUnitId;
        private string _recommendedAdUnitId;
        private double _calculatedBidFloor;
        private int _consecutiveAdFail;
        private bool _isLoadPending;
        private string _loadedAdUnitId;
        
        private readonly Action<string> _setStatus;
        private readonly Action _onLoad;
        
        public void OnBehaviourInsight(Dictionary<string, Insight> insights)
        {
            _recommendedAdUnitId = insights[AdUnitId]._string;
            _calculatedBidFloor = insights[FloorPrice]._float;
            
            Debug.Log($"OnBehaviourInsight for Rewarded recommended AdUnit: {_recommendedAdUnitId}, calculated bid floor: {_calculatedBidFloor}");

            _selectedAdUnitId = _recommendedAdUnitId;
            
            if (_isLoadPending)
            {
                Load();
            }
        }

        public Rewarded(Action requestNewInsight, Action<string> setStatus, Action onLoad)
        {
            GetInsights = requestNewInsight;
            
            _setStatus = setStatus;
            _onLoad = onLoad;
            
            MaxSdkCallbacks.Rewarded.OnAdLoadedEvent += OnAdLoadedEvent;
            MaxSdkCallbacks.Rewarded.OnAdLoadFailedEvent += OnAdFailedEvent;
            MaxSdkCallbacks.Rewarded.OnAdDisplayedEvent += OnAdDisplayedEvent;
        }

        public void Load()
        {
            _loadedAdUnitId = _selectedAdUnitId ?? _defaultAdUnitId;
            MaxSdk.LoadRewardedAd(_loadedAdUnitId);
        }
        
        public void Show()
        {
            if (MaxSdk.IsRewardedAdReady(_loadedAdUnitId))
            {
                _setStatus("Showing");
                MaxSdk.ShowRewardedAd(_loadedAdUnitId);
            }
            else
            {
                _setStatus("Ad not ready");
            }
        }
        
        private void OnAdLoadedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
        {
            NeftaAdapterEvents.OnExternalMediationRequestLoaded(NeftaAdapterEvents.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, adInfo);
            
            _setStatus($"Loaded {adInfo.NetworkName} {adInfo.NetworkPlacement}");

            _onLoad();
        }
        
        private void OnAdFailedEvent(string adUnitId, MaxSdkBase.ErrorInfo errorInfo)
        {
            NeftaAdapterEvents.OnExternalMediationRequestFailed(NeftaAdapterEvents.AdType.Rewarded, _recommendedAdUnitId, _calculatedBidFloor, adUnitId, errorInfo);
            
            if (errorInfo.Code == MaxSdkBase.ErrorCode.NoFill)
            {
                _consecutiveAdFail++;
                if (_consecutiveAdFail == 1) // in case of first no fill, try to get new insight (will probably return adUnit with lower bid floor
                {
                    _isLoadPending = true;
                    GetInsights();
                }
                else // for consequential no fills go with default (no bid floor) ad unit
                {
                    _selectedAdUnitId = null;
                    Load();
                }
            }
            
            _setStatus("Load failed");
        }
    }
      
      
    // Example usage from application entry point
    public class AdDemoController : MonoBehaviour
    {
        private void Awake()
        {
            NeftaAdapterEvents.EnableLogging(true);
            NeftaAdapterEvents.Init(NeftaId);
            NeftaAdapterEvents.SetContentRating(NeftaAdapterEvents.ContentRating.MatureAudience);

            NeftaAdapterEvents.BehaviourInsightCallback = OnBehaviourInsight;
            GetBehaviourInsight();
            
					  // the rest of awake logic ..
        }

        private void GetBehaviourInsight()
        {
            NeftaAdapterEvents.GetBehaviourInsight(new string[]
            {
                Banner.FloorPrice,
                Interstitial.AdUnitId, Interstitial.FloorPrice,
                Rewarded.AdUnitId, Rewarded.FloorPrice
            });
        }
        
        private void OnBehaviourInsight(Dictionary<string, Insight> behaviourInsight)
        {
            foreach (var insight in behaviourInsight)
            {
                var insightValue = insight.Value;
                Debug.Log($"BehaviourInsight {insight.Key} status:{insightValue._status} i:{insightValue._int} f:{insightValue._float} s:{insightValue._string}");
            }
            
            _banner.OnBehaviourInsight(behaviourInsight);
            _interstitial.OnBehaviourInsight(behaviourInsight);
            _rewarded.OnBehaviourInsight(behaviourInsight);
        }
     // ...