Bid floors in MAX

Setup in MAX

In the AppLovin dashboard there needs to be two ad units per ad format. One that will be used for optimal ad value with realtime bid floor setting (ie., the Nefta Intelligent Floor), and another one that will be used for backfill.

Neither one of them should have bid floor configured in the dashboard.

Integrate the AdSDK

You can integrate the MAX adSDK via Unity, Native iOS or Native Android.

Disable Back-to-back Ad Loading

❗️

To make sure each ad request with dynamic bid floor works correctly, disable back to back loading for adUnit that will have bid bid floor configured programatically in real time.

This has to be done before initializing MAX SDK:

private readonly string[] _adUnits = new string[] {
#if UNITY_IOS
  // interstitials // Replace with your ad unit ids
  "6d318f954e2630a8",
  // rewarded // Replace with your ad unit ids
  "918acf84edf9c034"
#else // UNITY_ANDROID
  // interstitials // Replace with your ad unit ids
  "7267e7f4187b95b2",
  // rewarded // Replace with your ad unit ids
  "72458470d47ee781"
#endif
};

MaxSdk.SetExtraParameter("disable_b2b_ad_unit_ids", string.Join(",", _adUnits));
MaxSdk.InitializeSdk();
let adUnits = [
  // interstitials
  "6d318f954e2630a8",
  // rewarded
  "918acf84edf9c034"
]

let max = ALSdk.shared()
max.settings.setExtraParameterForKey("disable_b2b_ad_unit_ids", value: adUnits.joined(separator: ","))
max.settings.isVerboseLoggingEnabled = true
let initConfig = ALSdkInitializationConfiguration(sdkKey: "key") { builder in
   builder.mediationProvider = ALMediationProviderMAX
}
max.initialize(with: initConfig) { sdkConfig in
}
ALSdkInitializationConfiguration *initConfig = [ALSdkInitializationConfiguration configurationWithSdkKey: @"key" builderBlock:^(ALSdkInitializationConfigurationBuilder *builder) {
   builder.mediationProvider = ALMediationProviderMAX;
}];
    
ALSdkSettings *settings = [ALSdk shared].settings;
NSArray *adUnits = @[
  // interstitials
  @"6d318f954e2630a8",
  // rewarded
  @"918acf84edf9c034"
];
[settings setExtraParameterForKey: @"disable_b2b_ad_unit_ids" value: [adUnits componentsJoinedByString:@","]];

[[ALSdk shared] initializeWithConfiguration: initConfig completionHandler:^(ALSdkConfiguration *sdkConfig) {
}];
String[] adUnits = new String[]{
  // interstitials
  "7267e7f4187b95b2",
  // rewarded
  "72458470d47ee781"
};

AppLovinSdk sdk = AppLovinSdk.getInstance(this);
sdk.getSettings().setExtraParameter("disable_b2b_ad_unit_ids", String.join(",", adUnits));
AppLovinSdkInitializationConfiguration initConfig = AppLovinSdkInitializationConfiguration.builder( "key" )
  .setMediationProvider( AppLovinMediationProvider.MAX )
  .build();
sdk.initialize( initConfig, new AppLovinSdk.SdkInitializationListener() {
  @Override
  public void onSdkInitialized(final AppLovinSdkConfiguration sdkConfig) {
    AppLovinSdk.getInstance(MainActivity.this).getSettings().setVerboseLogging(true);
  }
});
List<String> adUnits = Platform.isAndroid ?
  ["0822634ec9c39d78", "3d7ef05a78cf8615" ] :
  ["5e11b1838778c517", "ad9b024164e61c00" ];

AppLovinMAX.setExtraParameter("disable_b2b_ad_unit_ids", adUnits.join(","));

Requesting floor price insights

Via the SDK, you can requests insights:

  • Churn: ._churn._d1_probability, ...
  • User value bid floor price per ad format: ._banner._floorPrice, ._interstitial._floorPrice and ._rewarded._floorPrice.
  • Recommended AdUnit Id: ._banner._adUnit, ._interstitial._adUnit and._rewarded._adUnit (only for AdMob)
NeftaAdapterEvents.GetInsights(Insights.Rewarded, (Insights insiights) => {
   _usedInsight = insights._rewarded;
}, TimeoutInSeconds);
[NeftaPlugin._instance GetInsights: Insights.Interstitial callback: ^(Insights * insights) {
   _usedInsight = insights._interstitial;
} timeout: 5];
NeftaPlugin._instance.GetInsights(Insights.Rewarded, callback: { insights in
   _usedInsight = insights._rewarded
}, timeout: TimeoutInSeconds)
NeftaPlugin._instance.GetInsights(Insights.REWARDED, 
  (Insights insights) -> {
      _usedInsight = insights._rewarded;
   }, TimeoutInSeconds);
);
  
Nefta.getInsights(Insights.INTERSTITIAL, (Insights insights) => {
  _interstitialInsight = insights.interstitial
}, 5);

If you provide timeout parameter, you are guaranteed to receive callback within the specified time. Before using any insight, check that it is not null.

Log ad request

When making and request, either from insight (ie., personalised price for that user), requested an ad in MAX using the insights.[_banner|_interstitial|_rewarded]._floorPrice field or default (backfill) request log it with:

NeftaAdapterEvents.OnExternalMediationRequest(NeftaAdapterEvents.AdType.Rewarded, DynamicAdUnitId, _dynamicInsight);
// or
NeftaAdapterEvents.OnExternalMediationRequest(NeftaAdapterEvents.AdType.Rewarded, DefaultAdUnitId);
[ALNeftaMediationAdapter OnExternalMediationRequestWithInterstitial: _dynamicInterstitial insight: _dynamicInsight];
// or:
[ALNeftaMediationAdapter OnExternalMediationRequestWithInterstitial: _defaultInterstitial];
ALNeftaMediationAdapter.onExternalMediationRequest(withRewarded: _dynamicRewarded!, insight: insight)
// or:
ALNeftaMediationAdapter.onExternalMediationRequest(withRewarded: _defaultRewarded!)
NeftaMediationAdapter.OnExternalMediationRequest(_dynamicRewarded, _dynamicInsight);
// or:
NeftaMediationAdapter.OnExternalMediationRequest(_defaultRewarded);
Nefta.onExternalMediationRequest(AdType.Interstitial, _dynamicInterstitialAdUnitId, _dynamicInterstitialInsight);
// or:
Nefta.onExternalMediationRequest(AdType.Interstitial, _defaultInterstitialAdUnitId);

Log outcome of ad opportunity

The client should log the outcome of the ad opportunity in order to continuously maximise ad revenue uplift.

private void OnAdLoadedEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
  NeftaAdapterEvents.OnExternalMediationRequestLoaded(adInfo);
}

private void OnAdFailedEvent(string adUnitId, MaxSdkBase.ErrorInfo errorInfo)
{
  NeftaAdapterEvents.OnExternalMediationRequestFailed(adUnitId, errorInfo);
}
- (void)didLoadAd:(MAAd *)ad {
  [ALNeftaMediationAdapter OnExternalMediationRequestLoadWithInterstitial: _dynamicInterstitial ad: ad];
}

- (void)didFailToLoadAdForAdUnitIdentifier:(NSString *)adUnitIdentifier withError:(MAError *)error {
  [ALNeftaMediationAdapter OnExternalMediationRequestFailWithInterstitial: _dynamicInterstitial error: error];
}
func didLoad(_ ad: MAAd) {
    ALNeftaMediationAdapter.onExternalMediationRequestLoad(.rewarded, ad: ad, usedInsight: _usedInsight)
}

func didFailToLoadAd(forAdUnitIdentifier adUnitIdentifier: String, withError error: MAError) {
  ALNeftaMediationAdapter.onExternalMediationRequestFail(withRewarded: _dynamicRewarded!, error: error)
}
@Override
public void onAdLoaded(@NonNull MaxAd ad) {
  NeftaMediationAdapter.OnExternalMediationRequestLoaded(_dynamicRewarded, ad);
}

@Override
public void onAdLoadFailed(@NonNull String adUnitId, @NonNull MaxError maxError) {
  NeftaMediationAdapter.OnExternalMediationRequestFailed(_dynamicRewarded, maxError);
}
AppLovinMAX.setRewardedAdListener(RewardedAdListener(
  onAdLoadedCallback: (ad) {
    Nefta.onExternalMediationRequestLoaded(ad);
  },
  onAdLoadFailedCallback: (adUnitId, error) {
    Nefta.onExternalMediationRequestFailed(adUnitId, error);
  }
}

When an ad successfully shows log the impression with this:

// By default this impression is collected automatically in Unity.
// But in case you disable this (with sendAdEvents=false in plugin Init) and send impression manually:
MaxSdkCallbacks.Rewarded.OnAdRevenuePaidEvent += (adUnitId, info) =>
{
    NeftaAdapterEvents.OnExternalMediationImpression(adUnitId, info);
};
- (void)didPayRevenueForAd:(nonnull MAAd *)ad {
    [ALNeftaMediationAdapter OnExternalMediationImpression: ad];
}
func didPayRevenue(for ad: MAAd) {
  ALNeftaMediationAdapter.onExternalMediationImpression(ad)
}
@Override
public void onAdRevenuePaid(final MaxAd ad) {
    NeftaMediationAdapter.OnExternalMediationImpression(ad);
}
  onAdRevenuePaidCallback: (ad) {
    Nefta.onExternalMediationImpression(ad);
  }

When there is click recorded on an ad:

private void OnAdClickEvent(string adUnitId, MaxSdkBase.AdInfo adInfo)
{
  NeftaAdapterEvents.OnExternalMediationClick(adUnitId, adInfo);
}
- (void)didClickAd:(MAAd *)ad {
  [ALNeftaMediationAdapter OnExternalMediationClick: ad];
}
func didClick(_ ad: MAAd) {
  ALNeftaMediationAdapter.onExternalMediationClick(ad)
}
@Override
public void onAdClicked(@NonNull MaxAd ad) {
  NeftaMediationAdapter.OnExternalMediationClick(ad);
}
  onAdClickedCallback: (ad) {
    Nefta.onExternalMediationClick(ad);
  }

Initial Request Has No Fill

If the request with the initially recommended insight (price) doesn't return an ad:

  • Re-request the ad insight: 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.
  • Please note that MAX requires an exponential backoff delay between these retries, specified as 2, 4, 8, 16, 32, 64, 64, 64... seconds. As mentioned in the introduction, Nefta insures your ads are served within time windows you specify and balances fill %, revenue uplift and latency.

Complete example

Demonstration project: Integration example.