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 bidding networks and deliver ad revenue uplift to partners. Nefta has seen 10-20% ad revenue increase with a comprehensive use of bid floor prices. In order to effectively set floor prices, partners need advanced user value estimation models and market price or bid landscape models.

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 numerous additional ad units with a floor price set. The floor price will be set at different bucket intervals, for example: 1, 5, 10, 20, 50, 100, 500.

The right number of ad units

In general the more ad units with a floor price bucket per ad format, the better. If you have a limit of ad units per ad format, its suggested to talk to your Applovin account representative to increase. If this is not possible, start with as many as possible and optimise from there. Discuss with your Nefta account representative.

Setting the right floor prices

To get started, its suggested to use a best guess approach of value buckets for your users. Alternatively, discuss with your Nefta account representative to provide value buckets based on the ad data available.

Optimising floor buckets

  1. Using data to understand which floor price buckets are delivering the largest fill rate and splitting the floor bucket into new price buckets.
  2. Analysing predicted user value in the default ad unit to discover additional untapped value buckets to add.

Receive the dynamic bid floor for the user from Nefta

Firstly, integrate user insights and request the following user insight values:

  • Predicted floor price per user per ad format: calculated_user_floor_price_banner, calculated_user_floor_price_interstitial, calculated_user_floor_price_rewarded.

Select the right initial Ad Unit

Using the calculated_user_floor_price_* value as requested above, select the Ad unit with the closest bid floor price and call the function MaxSdk.LoadRewardedAd(adUnitId); in the case of a rewarded ad unit.

Ad unit selection logic:

  1. Partners can select the closest ad unit with a floor price thats lower than calculated_user_floor_price_*, this will result in a higher chance of fill but less optimisation.
  2. Partners can select the closest ad unit with a percentage difference, for example above/bellow 5%.

It's important to continuously optimise the ad unit selection, number of ad units and price buckets.

Initial Ad Unit has no fill

If the initially selected ad unit doesn't return an ad, either you can select an ad unit with a lower price bucket or select your default ad unit.

Example code

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

  struct PlacementConfig {
    var index: Int
    var placementId: String
    var cpm: Double
  }

  let _placements: [PlacementConfig] = [
    // Add as many placements as you like.
    PlacementConfig(index: 0, placementId: "b5231.....eb4dv", cpm: 0),
    PlacementConfig(index: 1, placementId: "a8431.....8eb4d1", cpm: 25.0),
    PlacementConfig(index: 2, placementId: "34682.....e9b052", cpm: 50.0),
    PlacementConfig(index: 3, placementId: "d066e.....d29f8b", cpm: 75.0),
  ]

  var _selectedPlacement: PlacementConfig?
  var _calculatedFloorPrice: Float64 = 0

  @objc func Show() {
    if _selectedPlacement == nil {
      _selectedPlacement = _placements.first

      if let insights = _insights {
        _calculatedFloorPrice = insights["calculated_user_floor_price_banner"]?._float ?? 0
       
        for placment in _placements {
          if placment.cpm > bid_floor_price {
            break
            // Could add a 5/10% flexibility if its up or down. Make sense to push higher
          }
          _selectedPlacement = placment
          // By logging the difference between placement price and bid_floor_price, partners can understand the value being left on the table which can be optimised by adding more placements.
        }
      }
    }

    _adView = MAAdView(adUnitIdentifier: _selectedPlacement!.placementId)
    _adView.delegate = self

    _adView.frame = CGRect(x: 0, y: 0, width: 320, height: 50)
    _bannerPlaceholder.addSubview(_adView)
    _adView.loadAd()
  }

  func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError)   {
    ALNeftaMediationAdapterSwift.OnExternalAdFail(.Banner, unitFloorPrice: _selectedPlacement!.cpm, calculatedFloorPrice: _calculatedFloorPrice, error: error)
    
    if error.code == .noFill {
      if _selectedPlacement!.index > 0 {
        _selectedPlacement = _placements[_selectedPlacement!.index - 1] 
        // Stategies:
        // 1. Simply continue going down until a placement delivers fill
        // 2. Select the closest Adplacement ± 5%, select the 2nd best, then go to defualt placement.


        // Alternatively, switch to default placement.
      }
    }
  }
  
  func didLoad(_ ad: MAAd) {
    ALNeftaMediationAdapterSwift.OnExternalAdLoad(.Banner, unitFloorPrice: _selectedPlacement!.cpm, calculatedFloorPrice: _calculatedFloorPrice)
    
    // Try to increase bid floor
    // if _selectedPlacement!.index < _placements.count - 1 {
    //   _selectedPlacement = _placements[_selectedPlacement!.index + 1]
    // }
  }
}