Trusted Arbiter
Compare CloudX bids with supported third-party bids in iOS apps
Trusted Arbiter compares a loaded CloudX bid with supported third-party bids and returns the selected platform. CloudX iOS SDK 3.4.0 supports CloudX, Unity LevelPlay, and PubMatic bid inputs. CloudX iOS SDK 3.5.0 and later also supports custom publisher-supplied bid inputs.
Supported ad formats
Trusted Arbiter is format-agnostic: it takes any loaded CloudX ad and compares it against the supplied third-party bids, regardless of format. Banner, MREC, Interstitial, and Rewarded are all supported.
- Interstitial and Rewarded (fullscreen formats) follow the step-by-step and controller pattern shown later on this page.
- Banner and MREC (view formats) require the additional handling described in Banner and MREC arbitration below, because the losing bid’s view must never be attached to the view hierarchy and the refresh cycle has to be coordinated manually.
Basic API
Create bid candidates from loaded ads, then pass them to the arbiter.
// cloudXAd is the CLXAd object from a CloudX load callback.
// levelPlayAdInfo is the Unity LevelPlay ad info object.
// pubMaticPrice and pubMaticPartnerName come from the PubMatic/OpenWrap bid object.
CLXArbiterBid *cloudXBid = [CLXArbiterBid cloudXBidWithAd:cloudXAd];
CLXArbiterBid *levelPlayBid =
[CLXArbiterBid levelPlayBidWithNetworkName:levelPlayAdInfo.adNetwork
revenue:levelPlayAdInfo.revenue.doubleValue
precision:levelPlayAdInfo.precision];
CLXArbiterBid *pubMaticBid =
[CLXArbiterBid pubMaticBidWithPrice:pubMaticPrice
partnerName:pubMaticPartnerName
extras:nil];
CLXArbiterConfiguration *configuration =
[CLXArbiterConfiguration configurationWithBids:@[cloudXBid, levelPlayBid, pubMaticBid]];
[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
NSLog(@"Selected platform: %@", result.platform.name);
}];// cloudXAd is the CLXAd object from a CloudX load callback.
// levelPlayAdInfo is the Unity LevelPlay ad info object.
// pubMaticPrice and pubMaticPartnerName come from the PubMatic/OpenWrap bid object.
let cloudXBid = CLXArbiterBid.cloudX(ad: cloudXAd)
let levelPlayBid = CLXArbiterBid.levelPlay(
networkName: levelPlayAdInfo.adNetwork,
revenue: levelPlayAdInfo.revenue?.doubleValue ?? 0,
precision: levelPlayAdInfo.precision
)
let pubMaticBid = CLXArbiterBid.pubMatic(
price: pubMaticPrice,
partnerName: pubMaticPartnerName,
extras: nil
)
let configuration = CLXArbiterConfiguration.configuration(
bids: [cloudXBid, levelPlayBid, pubMaticBid],
builderBlock: nil
)
CloudXCore.shared.arbiter(with: configuration) { result in
print("Selected platform: \(result.platform.name)")
}CLXArbiterBid.cloudX accepts the CLXAd object from a CloudX load callback. CLXArbiterBid.levelPlay accepts Unity LevelPlay ad info values. CLXArbiterBid.pubMatic accepts a PubMatic OpenWrap bid price and optional partner name. The extras map is optional on both the LevelPlay and PubMatic bids, and partnerName is optional on PubMatic. The completion callback runs on the main thread, so you can show an ad or update UI directly from it.
result.platform is CLXArbiterPlatform.cloudX, levelPlay, or pubMatic for the selected platform, or CLXArbiterPlatform.none when no winner could be selected (for example, no bids were supplied).
AdMob and Google Ad Manager
CloudX compares a loaded CloudX ad with a loaded AdMob or Google Ad Manager ad. AdMob and Ad Manager are separate demand sources, so both may bid in the same arbitration. This applies to publisher-managed mediation setups, including accounts described commercially as AdMob Pro; there is no separate AdMob Pro SDK API.
Google demand does not normally reveal the price of a loaded ad before it is shown, so there is no pre-bid price for the arbiter to compare against CloudX’s bid. CloudX estimates the bid price from the prior performance of similar ad units, so you do not need to supply a price. No pre-bid pricing API is required.
If your AdMob account exposes impression-level revenue data pre-bid, you can supply that exact price yourself instead of using the estimate — see Manual input with pre-bid ILRD below.
Report Google paid events back to CloudX (required)
Forwarding Google’s paid events is a required part of the Trusted Arbiter AdMob and Ad Manager integration, not an optional analytics extra. CloudX has no visibility into what a Google ad earned, so the estimate it bids with comes entirely from the realized prices you report back.
After you show an AdMob or Ad Manager ad that won an arbitration, forward Google’s impression-level revenue — the GADAdValue delivered to the ad’s paidEventHandler — into the CloudX SDK with reportRevenueData(_:). Use CLXRevenuePlatformAdMob for AdMob ads and CLXRevenuePlatformGAM for Ad Manager ads. Without this feedback CloudX never learns the realized prices for your ad units, and future arbiter estimates degrade.
GADAdValue.value is an NSDecimalNumber already expressed in currency units on iOS, so pass it straight through. Do not divide by 1,000,000 — only the Android and Unity Google Mobile Ads SDKs report ad values in micros.
func revenuePrecision(from precision: GADAdValuePrecision) -> CLXRevenuePrecision {
switch precision {
case .precise: return .exact
case .estimated: return .estimated
case .publisherProvided: return .publisherDefined
default: return .undefined
}
}
// Attach once to each Google ad you loaded as an arbiter candidate.
adMobInterstitial.paidEventHandler = { [weak adMobInterstitial] adValue in
let servedBy = adMobInterstitial?.responseInfo.loadedAdNetworkResponseInfo
let data = CLXRevenueData.revenueData(
platform: .adMob, // .gam for an Ad Manager ad
revenue: adValue.value.doubleValue,
adFormat: "interstitial"
) { builder in
builder.currencyCode = adValue.currencyCode
builder.precision = revenuePrecision(from: adValue.precision)
builder.networkName = servedBy?.adSourceName
builder.adUnitId = adMobAdUnitId
builder.thirdPartyAdPlacementId = servedBy?.adSourceInstanceName
}
CloudXCore.shared.reportRevenueData(data)
}static CLXRevenuePrecision *CLXRevenuePrecisionFromGAD(GADAdValuePrecision precision) {
switch (precision) {
case GADAdValuePrecisionPrecise: return CLXRevenuePrecision.exact;
case GADAdValuePrecisionEstimated: return CLXRevenuePrecision.estimated;
case GADAdValuePrecisionPublisherProvided: return CLXRevenuePrecision.publisherDefined;
case GADAdValuePrecisionUnknown: return CLXRevenuePrecision.undefined;
}
return CLXRevenuePrecision.undefined;
}
// Attach once to each Google ad you loaded as an arbiter candidate.
__weak GADInterstitialAd *weakAd = adMobInterstitial;
adMobInterstitial.paidEventHandler = ^(GADAdValue *adValue) {
GADAdNetworkResponseInfo *servedBy = weakAd.responseInfo.loadedAdNetworkResponseInfo;
CLXRevenueData *data =
[CLXRevenueData revenueDataWithPlatform:CLXRevenuePlatformAdMob // CLXRevenuePlatformGAM for Ad Manager
revenue:adValue.value.doubleValue
adFormat:@"interstitial"
builderBlock:^(CLXRevenueDataBuilder *builder) {
builder.currencyCode = adValue.currencyCode;
builder.precision = CLXRevenuePrecisionFromGAD(adValue.precision);
builder.networkName = servedBy.adSourceName;
builder.adUnitId = adMobAdUnitId;
builder.thirdPartyAdPlacementId = servedBy.adSourceInstanceName;
}];
[[CloudXCore shared] reportRevenueData:data];
};Attach the handler once per ad object. AdMob re-fires paid events on banner and MREC auto-refresh against the same view, so a handler installed once keeps reporting every refreshed impression. See Publisher-Reported Revenue Data for the full reporting setup and the complete field list.
Create the bid from the ad unit id of the ad you loaded:
// An AdMob ad unit. networkName is optional; pass the winning ad source when you know it,
// e.g. responseInfo.loadedAdapterResponseInfo?.adSourceName.
let adMobBid = CLXArbiterBid.adMob(
adUnitId: adMobAdUnitId,
networkName: adMobNetworkName ?? "admob",
manualRevenuePerImpressionUSD: nil,
extras: [:]
)
// An Ad Manager ad unit id takes the form /NNNNNNN/placement/name.
let adManagerBid = CLXArbiterBid.gam(adUnitId: "/21775744923/example/interstitial")
let configuration = CLXArbiterConfiguration.configuration(
bids: [CLXArbiterBid.cloudX(ad: cloudXAd), adMobBid, adManagerBid],
builderBlock: nil
)
CloudXCore.shared.arbiter(with: configuration) { result in
switch result.platform.name {
case CLXArbiterPlatform.cloudX.name:
cloudXInterstitial.show(from: viewController)
case CLXArbiterPlatform.adMob.name:
adMobInterstitial.present(fromRootViewController: viewController)
case CLXArbiterPlatform.gam.name:
adManagerInterstitial.present(fromRootViewController: viewController)
default:
break
}
}CLXArbiterBid *adMobBid =
[CLXArbiterBid adMobBidWithAdUnitId:adMobAdUnitId
networkName:adMobNetworkName ?: @"admob"
manualRevenuePerImpressionUSD:nil
extras:@{}];
CLXArbiterBid *adManagerBid =
[CLXArbiterBid gamBidWithAdUnitId:@"/21775744923/example/interstitial"];
CLXArbiterConfiguration *configuration =
[CLXArbiterConfiguration configurationWithBids:@[
[CLXArbiterBid cloudXBidWithAd:cloudXAd],
adMobBid,
adManagerBid
]];
[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
if ([result.platform.name isEqualToString:CLXArbiterPlatform.cloudX.name]) {
[cloudXInterstitial showFromViewController:viewController];
} else if ([result.platform.name isEqualToString:CLXArbiterPlatform.adMob.name]) {
[adMobInterstitial presentFromRootViewController:viewController];
} else if ([result.platform.name isEqualToString:CLXArbiterPlatform.gam.name]) {
[adManagerInterstitial presentFromRootViewController:viewController];
}
}];A winning Google bid reports its own platform — CLXArbiterPlatform.adMob or CLXArbiterPlatform.gam. You no longer inspect result.platformName to tell these sources apart, as you would for a custom bid.
Pass a blank ad unit id and the bid still builds rather than crashing your app, but it carries no usable identity: it is never priced and the server rejects it.
Manual input with pre-bid ILRD
Some AdMob accounts expose impression-level revenue data pre-bid: the GADAdValue for the loaded ad is available at load time, before the ad is shown. This is a legacy, account-gated capability, so check with your Google account team whether it is enabled for your account. An exact per-impression price that you know before show is the one case where supplying your own price beats CloudX’s estimate.
Pass the pre-bid ad value as manualRevenuePerImpressionUSD and it overrides the estimate:
// GADAdValue.value is an NSDecimalNumber already expressed in currency units,
// so pass it through as-is. Do not divide it by anything.
let adMobBid = CLXArbiterBid.adMob(
adUnitId: adMobAdUnitId,
networkName: adMobNetworkName ?? "admob",
manualRevenuePerImpressionUSD: preBidAdValue.value,
extras: [:]
)// GADAdValue.value is an NSDecimalNumber already expressed in currency units,
// so pass it through as-is. Do not divide it by anything.
CLXArbiterBid *adMobBid =
[CLXArbiterBid adMobBidWithAdUnitId:adMobAdUnitId
networkName:adMobNetworkName ?: @"admob"
manualRevenuePerImpressionUSD:preBidAdValue.value
extras:@{}];How the value is treated:
0.0is a real price. It means this bid is worth nothing — not that the price is missing.- Negative and non-finite values are not prices, so they are treated as absent and logged.
- A blank ad unit id drops the manual price entirely, because a bid with no identity cannot be validated.
manualRevenuePerImpressionUSD is revenue for a single impression, in USD — not CPM. Convert whatever your source reports:
- An AdMob ad value needs no scaling on iOS.
GADAdValue.valueis anNSDecimalNumberalready expressed in currency units, so a value of0.005is0.005per impression. Pass it through as-is — do not divide by 1,000, and do not divide by 1,000,000 either. Only the Android and Unity Google Mobile Ads SDKs report ad values in micros. - A non-USD amount must be converted to USD first.
Step-by-step: arbitrate CloudX and LevelPlay
This walkthrough shows exactly which Unity LevelPlay callback to read and which values to pass into the arbiter. It uses an interstitial, but the same field mapping applies to any format — see Supported ad formats for what changes with Banner and MREC.
Load both candidates
Create the CloudX and LevelPlay interstitials, set their delegates, and start a load on each platform.
self.cloudXInterstitial = [[CloudXCore shared] createInterstitialWithAdUnitId:@"YOUR_CLOUDX_AD_UNIT_ID"];
self.cloudXInterstitial.delegate = self;
[self.cloudXInterstitial load];
self.levelPlayInterstitial = [[LPMInterstitialAd alloc] initWithAdUnitId:@"YOUR_LEVELPLAY_AD_UNIT_ID"];
self.levelPlayInterstitial.delegate = self;
[self.levelPlayInterstitial loadAd];cloudXInterstitial = CloudXCore.shared.createInterstitial(adUnitId: "YOUR_CLOUDX_AD_UNIT_ID")
cloudXInterstitial?.delegate = self
cloudXInterstitial?.load()
levelPlayInterstitial = LPMInterstitialAd(adUnitId: "YOUR_LEVELPLAY_AD_UNIT_ID")
levelPlayInterstitial.setDelegate(self)
levelPlayInterstitial.loadAd()Capture each platform's loaded ad
LevelPlay delivers an LPMAdInfo in its load callback; CloudX delivers a CLXAd. Hold onto both because you read the arbiter inputs from them in the next step.
// Properties: @property (nonatomic, strong) CLXAd *cloudXAd;
// @property (nonatomic, strong) LPMAdInfo *levelPlayInfo;
// CLXInterstitialDelegate
- (void)didLoadAd:(CLXAd *)ad {
self.cloudXAd = ad;
}
// LPMInterstitialAdDelegate
- (void)didLoadAdWithAdInfo:(LPMAdInfo *)adInfo {
self.levelPlayInfo = adInfo;
}var cloudXAd: CLXAd?
var levelPlayInfo: LPMAdInfo?
// CLXInterstitialDelegate
func didLoad(_ ad: CLXAd) {
cloudXAd = ad
}
// LPMInterstitialAdDelegate
func didLoadAd(with adInfo: LPMAdInfo) {
levelPlayInfo = adInfo
}Map the values into bids
Read the LevelPlay fields off LPMAdInfo and pass them to CLXArbiterBid.levelPlay. The CloudX bid takes the CLXAd directly. Submit only the platforms that actually loaded.
LPMAdInfo field | Type | CLXArbiterBid.levelPlay parameter |
|---|---|---|
adNetwork | NSString * | networkName |
revenue | NSNumber * | revenue (unwrap with .doubleValue) |
precision | NSString * | precision |
NSMutableArray<CLXArbiterBid *> *bids = [NSMutableArray array];
if (self.cloudXAd) {
[bids addObject:[CLXArbiterBid cloudXBidWithAd:self.cloudXAd]];
}
if (self.levelPlayInfo) {
CLXArbiterBid *levelPlayBid =
[CLXArbiterBid levelPlayBidWithNetworkName:self.levelPlayInfo.adNetwork
revenue:self.levelPlayInfo.revenue.doubleValue
precision:self.levelPlayInfo.precision];
[bids addObject:levelPlayBid];
}
CLXArbiterConfiguration *configuration =
[CLXArbiterConfiguration configurationWithBids:bids];var bids: [CLXArbiterBid] = []
if let cloudXAd {
bids.append(CLXArbiterBid.cloudX(ad: cloudXAd))
}
if let levelPlayInfo {
bids.append(CLXArbiterBid.levelPlay(
networkName: levelPlayInfo.adNetwork,
revenue: levelPlayInfo.revenue?.doubleValue ?? 0,
precision: levelPlayInfo.precision
))
}
let configuration = CLXArbiterConfiguration.configuration(
bids: bids,
builderBlock: nil
)Run the arbiter
Pass the configuration to the arbiter with a completion handler. Run it only after both interstitials have settled: track each load callback and load failure, then submit only the candidates that loaded. The completion runs on the main thread.
[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
[self showWinner:result];
}];CloudXCore.shared.arbiter(with: configuration) { [weak self] result in
self?.showWinner(result)
}Show the winner
Compare result.platform.name against the platform constants and show the winning platform’s ad. CLXArbiterPlatform.none means no winner was selected, so continue without showing an ad.
- (void)showWinner:(CLXArbiterResult *)result {
NSString *platform = result.platform.name;
if ([platform isEqualToString:CLXArbiterPlatform.cloudX.name]) {
[self.cloudXInterstitial showFromViewController:self];
} else if ([platform isEqualToString:CLXArbiterPlatform.levelPlay.name]) {
[self.levelPlayInterstitial showAdWithViewController:self placementName:nil];
}
// CLXArbiterPlatform.none: no winner; continue without an ad
}func showWinner(_ result: CLXArbiterResult) {
switch result.platform.name {
case CLXArbiterPlatform.cloudX.name:
cloudXInterstitial?.show(from: self)
case CLXArbiterPlatform.levelPlay.name:
levelPlayInterstitial.showAd(viewController: self, placementName: nil)
default:
break // CLXArbiterPlatform.none: no winner; continue without an ad
}
}The ArbiterInterstitialController below packages these same steps into a reusable component that prepares a winner ahead of the placement.
Interstitial example
This interstitial example arbitrates between two platforms: CloudX and Unity LevelPlay. Prepare a winner before the placement is reached:
- Load CloudX and LevelPlay in parallel.
- Wait until both platforms have loaded or failed.
- Submit only loaded candidates to Trusted Arbiter.
- Cache the selected platform.
- At the placement, show the cached winner immediately.
If both platforms fail, start another load cycle. If the placement is reached before a winner is prepared, continue the app flow without showing an ad.
/// Prepares a Trusted Arbiter winner ahead of time so an interstitial can be shown
/// instantly when a placement is reached.
///
/// Loads the CloudX and LevelPlay interstitials in parallel, waits until both have
/// finished loading or failing, submits the loaded candidates to CloudXCore.shared.arbiter,
/// and caches the selected CLXArbiterPlatform in nextWinner.
final class ArbiterInterstitialController: NSObject {
protocol Listener: AnyObject {
/// Called when the arbiter has selected a platform for the next show.
func arbiterInterstitialController(
_ controller: ArbiterInterstitialController,
didPrepareWinner platform: CLXArbiterPlatform
)
}
weak var listener: Listener?
private let cloudXInterstitial: CLXInterstitial
private let levelPlayInterstitial: LPMInterstitialAd
private var cloudXAd: CLXAd?
private var cloudXLoadDone = false
private var levelPlayAdInfo: LPMAdInfo?
private var levelPlayLoadDone = false
private var nextWinner: CLXArbiterPlatform?
init(cloudXInterstitial: CLXInterstitial, levelPlayInterstitial: LPMInterstitialAd) {
self.cloudXInterstitial = cloudXInterstitial
self.levelPlayInterstitial = levelPlayInterstitial
super.init()
self.cloudXInterstitial.delegate = self
self.levelPlayInterstitial.setDelegate(self)
}
/// Starts a load for each platform that does not currently hold a cached ad.
func loadMissingAds() {
if cloudXAd == nil { cloudXInterstitial.load() }
if levelPlayAdInfo == nil { levelPlayInterstitial.loadAd() }
}
/// Shows the prepared winner, returning true only when a show call was made.
///
/// Returns false when no winner is ready or the cached ad is no longer available, in which
/// case a fresh load cycle is started.
func showAtPlacement(from viewController: UIViewController, placementName: String? = nil) -> Bool {
guard let platformName = nextWinner?.name else { return false }
if platformName == CLXArbiterPlatform.cloudX.name {
return showCloudX(from: viewController, placementName: placementName)
}
if platformName == CLXArbiterPlatform.levelPlay.name {
return showLevelPlay(from: viewController, placementName: placementName)
}
return false
}
/// Runs the arbiter once both platforms have settled, then caches the winning platform.
///
/// Returns early until both loads complete. If neither platform loaded, it restarts the
/// load cycle; otherwise it submits the loaded candidates to CloudXCore.shared.arbiter.
private func maybePrepareWinner() {
guard cloudXLoadDone, levelPlayLoadDone else { return }
if cloudXAd == nil && levelPlayAdInfo == nil {
cloudXLoadDone = false
levelPlayLoadDone = false
loadMissingAds()
return
}
var bids: [CLXArbiterBid] = []
if let cloudXAd {
bids.append(CLXArbiterBid.cloudX(ad: cloudXAd))
}
if let levelPlayAdInfo {
bids.append(CLXArbiterBid.levelPlay(
networkName: levelPlayAdInfo.adNetwork,
revenue: levelPlayAdInfo.revenue?.doubleValue ?? 0,
precision: levelPlayAdInfo.precision
))
}
let configuration = CLXArbiterConfiguration.configuration(bids: bids, builderBlock: nil)
CloudXCore.shared.arbiter(with: configuration) { [weak self] result in
guard let self else { return }
nextWinner = result.platform
listener?.arbiterInterstitialController(self, didPrepareWinner: result.platform)
}
}
private func showCloudX(from viewController: UIViewController, placementName: String?) -> Bool {
if cloudXInterstitial.isReady {
if let placementName {
cloudXInterstitial.show(from: viewController, placement: placementName, customData: nil)
} else {
cloudXInterstitial.show(from: viewController)
}
return true
}
clearCloudXAndLoadMissingAds()
return false
}
private func showLevelPlay(from viewController: UIViewController, placementName: String?) -> Bool {
if levelPlayInterstitial.isAdReady() {
levelPlayInterstitial.showAd(viewController: viewController, placementName: placementName)
return true
}
clearLevelPlayAndLoadMissingAds()
return false
}
private func clearCloudXAndLoadMissingAds() {
cloudXAd = nil
cloudXLoadDone = false
nextWinner = nil
loadMissingAds()
}
private func clearLevelPlayAndLoadMissingAds() {
levelPlayAdInfo = nil
levelPlayLoadDone = false
nextWinner = nil
loadMissingAds()
}
}
extension ArbiterInterstitialController: CLXInterstitialDelegate {
func didLoad(_ ad: CLXAd) {
cloudXAd = ad
cloudXLoadDone = true
maybePrepareWinner()
}
func didFailToLoadAd(_ adUnitId: String, error: CLXError) {
cloudXAd = nil
cloudXLoadDone = true
maybePrepareWinner()
}
func didDisplay(_ ad: CLXAd) {}
func didFailToDisplay(_ ad: CLXAd, error: CLXError) {
clearCloudXAndLoadMissingAds()
}
func didHide(_ ad: CLXAd) {
clearCloudXAndLoadMissingAds()
}
func didClick(_ ad: CLXAd) {}
}
extension ArbiterInterstitialController: LPMInterstitialAdDelegate {
func didLoadAd(with adInfo: LPMAdInfo) {
levelPlayAdInfo = adInfo
levelPlayLoadDone = true
maybePrepareWinner()
}
func didFailToLoadAd(withAdUnitId adUnitId: String, error: Error) {
levelPlayAdInfo = nil
levelPlayLoadDone = true
maybePrepareWinner()
}
func didChangeAdInfo(_ adInfo: LPMAdInfo) {
levelPlayAdInfo = adInfo
}
func didDisplayAd(with adInfo: LPMAdInfo) {}
func didFailToDisplayAd(with adInfo: LPMAdInfo, error: Error) {
clearLevelPlayAndLoadMissingAds()
}
func didCloseAd(with adInfo: LPMAdInfo) {
clearLevelPlayAndLoadMissingAds()
}
func didClickAd(with adInfo: LPMAdInfo) {}
}showAtPlacement(from:placementName:) returns true only when an ad show call was made. didChangeAdInfo(_:) keeps the cached LevelPlay candidate up to date while it remains loaded.
For PubMatic OpenWrap, create a third-party bid with CLXArbiterBid.pubMatic(price:partnerName:extras:). If the arbiter service is unavailable, the SDK falls back to the highest comparable USD bid among the supplied supported bid inputs.
Banner and MREC arbitration
Banner and MREC are view-based formats: every candidate network renders its ad into a view as soon as it loads, whether or not that view ends up on screen. Trusted Arbiter still selects the winner the same way, but you take on two responsibilities that fullscreen formats don’t have.
Disable auto-refresh
Trusted Arbiter needs full control over when a new fill is requested and when the displayed ad changes, so each network’s own refresh timer must be off:
- Disable auto-refresh for the ad unit in the CloudX dashboard.
- Call
stopAutoRefreshon theCLXBannerAdViewimmediately after creating it (see Banner Ads (320x50)). - Disable auto-refresh on the equivalent API for every other network you arbitrate against.
View attachment
Only the winning bid’s view may be added to the view hierarchy. A losing network’s banner view still renders and fires its own impression the moment it is attached to a superview, so hold every non-winning view off-screen (do not call addSubview:/addSubview(_:) on it) until, or unless, it wins a later round. This differs from the standard banner integration, which adds the view with addSubview at creation time — with Trusted Arbiter, the view must not be attached at creation, only after arbitration selects it as the winner.
Refresh cycle
With auto-refresh off, drive the cycle yourself:
- Run the parallel loads, submit the loaded candidates to the arbiter, and attach the winner’s view.
- As soon as the winner’s impression fires, immediately start loading a new fill from the winning network.
- Retain the non-winning networks’ already-filled ads for the next round. Only re-request a load from a network that did not fill in the previous round.
- Once the outstanding load responses come back, run the arbiter again over the current set of loaded candidates.
- Refresh the displayed ad on a 20-30 second interval, swapping in the new winner’s view each time. Refreshing faster than 20 seconds decreases CPM performance.
Example
ArbiterBannerController below arbitrates a CloudX and LevelPlay banner, keeping exactly one view attached at a time and driving the refresh cycle described above.
/// Arbitrates a CloudX and LevelPlay banner on a 20-30 second refresh cycle.
///
/// Attaches only the winning bid's view. Non-winning views are kept loaded but
/// detached so they never render or fire an impression. After the displayed
/// winner's impression fires, starts a new load from that network and keeps
/// the other network's already-filled ad for the next arbitration round.
@interface ArbiterBannerController () <CLXBannerDelegate, CLXAdRevenueDelegate, LPMBannerAdViewDelegate>
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, weak) UIViewController *presentingViewController;
@property (nonatomic, strong) CLXBannerAdView *cloudXBanner;
@property (nonatomic, strong) LPMBannerAdView *levelPlayBanner;
@property (nonatomic, strong) CLXAd *cloudXAd;
@property (nonatomic, assign) BOOL cloudXLoadDone;
@property (nonatomic, strong) LPMAdInfo *levelPlayAdInfo;
@property (nonatomic, assign) BOOL levelPlayLoadDone;
@property (nonatomic, copy) NSString *attachedPlatformName;
@property (nonatomic, strong) NSTimer *refreshTimer;
@end
@implementation ArbiterBannerController
- (instancetype)initWithContainerView:(UIView *)containerView
presentingViewController:(UIViewController *)presentingViewController
cloudXAdUnitId:(NSString *)cloudXAdUnitId
levelPlayAdUnitId:(NSString *)levelPlayAdUnitId {
self = [super init];
if (self) {
_containerView = containerView;
_presentingViewController = presentingViewController;
_cloudXBanner = [[CloudXCore shared] createBannerWithAdUnitId:cloudXAdUnitId];
_cloudXBanner.delegate = self;
_cloudXBanner.revenueDelegate = self;
[_cloudXBanner stopAutoRefresh];
LPMBannerAdViewConfigBuilder *levelPlayConfigBuilder = [[LPMBannerAdViewConfigBuilder alloc] init];
LPMBannerAdViewConfig *levelPlayConfig = [levelPlayConfigBuilder build];
_levelPlayBanner = [[LPMBannerAdView alloc] initWithAdUnitId:levelPlayAdUnitId
config:levelPlayConfig];
_levelPlayBanner.delegate = self;
// LevelPlay auto-refresh is disabled via LevelPlay's own dashboard/API configuration.
}
return self;
}
/// Starts a load for each network that does not currently hold a filled ad.
- (void)loadMissingAds {
if (!self.cloudXAd) { [self.cloudXBanner load]; }
if (!self.levelPlayAdInfo) {
[self.levelPlayBanner loadAdWithViewController:self.presentingViewController];
}
}
/// Starts the recurring 20-30 second refresh timer. Call once, after the first load cycle begins.
- (void)startRefreshTimer {
[self.refreshTimer invalidate];
self.refreshTimer = [NSTimer scheduledTimerWithTimeInterval:25.0
target:self
selector:@selector(runArbiterIfReady)
userInfo:nil
repeats:YES];
}
- (void)runArbiterIfReady {
if (!self.cloudXLoadDone || !self.levelPlayLoadDone) { return; }
NSMutableArray<CLXArbiterBid *> *bids = [NSMutableArray array];
if (self.cloudXAd) {
[bids addObject:[CLXArbiterBid cloudXBidWithAd:self.cloudXAd]];
}
if (self.levelPlayAdInfo) {
[bids addObject:[CLXArbiterBid levelPlayBidWithNetworkName:self.levelPlayAdInfo.adNetwork
revenue:self.levelPlayAdInfo.revenue.doubleValue
precision:self.levelPlayAdInfo.precision]];
}
if (bids.count == 0) { return; }
CLXArbiterConfiguration *configuration = [CLXArbiterConfiguration configurationWithBids:bids];
[[CloudXCore shared] arbiterWithConfiguration:configuration completion:^(CLXArbiterResult *result) {
[self attachWinner:result.platform.name];
}];
}
/// Detaches the previous winner's view, attaches the new winner's view, and starts a
/// fresh load from the winning network once its impression fires.
- (void)attachWinner:(NSString *)platformName {
[self.cloudXBanner removeFromSuperview];
[self.levelPlayBanner removeFromSuperview];
if ([platformName isEqualToString:CLXArbiterPlatform.cloudX.name]) {
[self.containerView addSubview:self.cloudXBanner];
self.attachedPlatformName = platformName;
} else if ([platformName isEqualToString:CLXArbiterPlatform.levelPlay.name]) {
[self.containerView addSubview:self.levelPlayBanner];
self.attachedPlatformName = platformName;
} else {
self.attachedPlatformName = nil;
}
}
#pragma mark - CLXBannerDelegate
- (void)didLoadAd:(CLXAd *)ad {
self.cloudXAd = ad;
self.cloudXLoadDone = YES;
[self runArbiterIfReady];
}
- (void)didFailToLoadAd:(NSString *)adUnitId error:(CLXError *)error {
self.cloudXAd = nil;
self.cloudXLoadDone = YES;
[self runArbiterIfReady];
}
#pragma mark - CLXAdRevenueDelegate
- (void)didPayRevenueForAd:(CLXAd *)ad {
if ([self.attachedPlatformName isEqualToString:CLXArbiterPlatform.cloudX.name]) {
self.cloudXAd = nil;
self.cloudXLoadDone = NO;
[self.cloudXBanner load];
}
}
#pragma mark - LPMBannerAdViewDelegate
- (void)didLoadAdWithAdInfo:(LPMAdInfo *)adInfo {
self.levelPlayAdInfo = adInfo;
self.levelPlayLoadDone = YES;
[self runArbiterIfReady];
}
- (void)didFailToLoadAdWithAdUnitId:(NSString *)adUnitId error:(NSError *)error {
self.levelPlayAdInfo = nil;
self.levelPlayLoadDone = YES;
[self runArbiterIfReady];
}
- (void)didDisplayAdWithAdInfo:(LPMAdInfo *)adInfo {
if ([self.attachedPlatformName isEqualToString:CLXArbiterPlatform.levelPlay.name]) {
self.levelPlayAdInfo = nil;
self.levelPlayLoadDone = NO;
[self.levelPlayBanner loadAdWithViewController:self.presentingViewController];
}
}
@end/// Arbitrates a CloudX and LevelPlay banner on a 20-30 second refresh cycle.
///
/// Attaches only the winning bid's view. Non-winning views are kept loaded but
/// detached so they never render or fire an impression. After the displayed
/// winner's impression fires, starts a new load from that network and keeps
/// the other network's already-filled ad for the next arbitration round.
final class ArbiterBannerController: NSObject {
private weak var containerView: UIView?
private weak var presentingViewController: UIViewController?
private let cloudXBanner: CLXBannerAdView
private let levelPlayBanner: LPMBannerAdView
private var cloudXAd: CLXAd?
private var cloudXLoadDone = false
private var levelPlayAdInfo: LPMAdInfo?
private var levelPlayLoadDone = false
private var attachedPlatformName: String?
private var refreshTimer: Timer?
init(containerView: UIView, presentingViewController: UIViewController, cloudXAdUnitId: String, levelPlayAdUnitId: String) {
self.containerView = containerView
self.presentingViewController = presentingViewController
cloudXBanner = CloudXCore.shared.createBanner(adUnitId: cloudXAdUnitId)
let levelPlayConfig = LPMBannerAdViewConfigBuilder().build()
levelPlayBanner = LPMBannerAdView(adUnitId: levelPlayAdUnitId, config: levelPlayConfig)
super.init()
cloudXBanner.delegate = self
cloudXBanner.revenueDelegate = self
cloudXBanner.stopAutoRefresh()
levelPlayBanner.setDelegate(self)
// LevelPlay auto-refresh is disabled via LevelPlay's own dashboard/API configuration.
}
/// Starts a load for each network that does not currently hold a filled ad.
func loadMissingAds() {
if cloudXAd == nil { cloudXBanner.load() }
if levelPlayAdInfo == nil, let presentingViewController {
levelPlayBanner.loadAd(with: presentingViewController)
}
}
/// Starts the recurring 20-30 second refresh timer. Call once, after the first load cycle begins.
func startRefreshTimer() {
refreshTimer?.invalidate()
refreshTimer = Timer.scheduledTimer(withTimeInterval: 25.0, repeats: true) { [weak self] _ in
self?.runArbiterIfReady()
}
}
private func runArbiterIfReady() {
guard cloudXLoadDone, levelPlayLoadDone else { return }
var bids: [CLXArbiterBid] = []
if let cloudXAd {
bids.append(CLXArbiterBid.cloudX(ad: cloudXAd))
}
if let levelPlayAdInfo {
bids.append(CLXArbiterBid.levelPlay(
networkName: levelPlayAdInfo.adNetwork,
revenue: levelPlayAdInfo.revenue?.doubleValue ?? 0,
precision: levelPlayAdInfo.precision
))
}
guard !bids.isEmpty else { return }
let configuration = CLXArbiterConfiguration.configuration(bids: bids, builderBlock: nil)
CloudXCore.shared.arbiter(with: configuration) { [weak self] result in
self?.attachWinner(result.platform.name)
}
}
/// Detaches the previous winner's view, attaches the new winner's view, and starts a
/// fresh load from the winning network once its impression fires.
private func attachWinner(_ platformName: String) {
cloudXBanner.removeFromSuperview()
levelPlayBanner.removeFromSuperview()
switch platformName {
case CLXArbiterPlatform.cloudX.name:
containerView?.addSubview(cloudXBanner)
attachedPlatformName = platformName
case CLXArbiterPlatform.levelPlay.name:
containerView?.addSubview(levelPlayBanner)
attachedPlatformName = platformName
default:
attachedPlatformName = nil
}
}
}
extension ArbiterBannerController: CLXBannerDelegate {
func didLoad(_ ad: CLXAd) {
cloudXAd = ad
cloudXLoadDone = true
runArbiterIfReady()
}
func didFailToLoadAd(_ adUnitId: String, error: CLXError) {
cloudXAd = nil
cloudXLoadDone = true
runArbiterIfReady()
}
}
extension ArbiterBannerController: CLXAdRevenueDelegate {
func didPayRevenue(for ad: CLXAd) {
guard attachedPlatformName == CLXArbiterPlatform.cloudX.name else { return }
cloudXAd = nil
cloudXLoadDone = false
cloudXBanner.load()
}
}
extension ArbiterBannerController: LPMBannerAdViewDelegate {
func didLoadAd(with adInfo: LPMAdInfo) {
levelPlayAdInfo = adInfo
levelPlayLoadDone = true
runArbiterIfReady()
}
func didFailToLoadAd(withAdUnitId adUnitId: String, error: Error) {
levelPlayAdInfo = nil
levelPlayLoadDone = true
runArbiterIfReady()
}
func didDisplayAd(with adInfo: LPMAdInfo) {
guard attachedPlatformName == CLXArbiterPlatform.levelPlay.name,
let presentingViewController else { return }
levelPlayAdInfo = nil
levelPlayLoadDone = false
levelPlayBanner.loadAd(with: presentingViewController)
}
}didPayRevenue(for:) (CloudX’s impression signal) and didDisplayAd(with:) (LevelPlay’s impression signal) are what trigger the next load for whichever network is currently attached; the losing network’s already-filled ad is left untouched until it either wins a round or is consumed. runArbiterIfReady() is invoked both by load callbacks and by the refresh timer, so a round only actually swaps the attached view when both networks have settled.
Custom Bid Inputs
Use CLXArbiterBid.custom(...) when you want Trusted Arbiter to compare CloudX with a third-party platform that does not have a dedicated bid helper.
CLXArbiterBid *customBid =
[CLXArbiterBid customBidWithPlatformName:@"my_mediation_platform"
networkName:@"winning_demand_source"
revenuePerImpressionUSD:0.00125
precision:CLXArbiterPrecision.exact
extras:@{@"ad_unit": @"third-party-ad-unit-id"}];
CLXArbiterConfiguration *configuration =
[CLXArbiterConfiguration configurationWithBids:@[
[CLXArbiterBid cloudXBidWithAd:cloudXAd],
customBid
]];let customBid = CLXArbiterBid.custom(
platformName: "my_mediation_platform",
networkName: "winning_demand_source",
revenuePerImpressionUSD: 0.00125,
precision: .exact,
extras: ["ad_unit": "third-party-ad-unit-id"]
)
let configuration = CLXArbiterConfiguration.configuration(
bids: [CLXArbiterBid.cloudX(ad: cloudXAd), customBid],
builderBlock: nil
)When a custom bid wins, result.platform is CLXArbiterPlatform.custom and result.platformName contains the platformName supplied on the bid. Pass revenuePerImpressionUSD as revenue for one impression in USD, not CPM. Use CLXArbiterPrecision.exact, estimated, publisherDefined, or undefined to describe that revenue value.