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);
}];

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)
}

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
    }
}

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: [:]
)

How the value is treated:

  • 0.0 is 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.value is an NSDecimalNumber already expressed in currency units, so a value of 0.005 is 0.005 per 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];

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;
}

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 fieldTypeCLXArbiterBid.levelPlay parameter
adNetworkNSString *networkName
revenueNSNumber *revenue (unwrap with .doubleValue)
precisionNSString *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];

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];
}];

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
}

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:

  1. Load CloudX and LevelPlay in parallel.
  2. Wait until both platforms have loaded or failed.
  3. Submit only loaded candidates to Trusted Arbiter.
  4. Cache the selected platform.
  5. 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.

ArbiterInterstitialController.swift
/// 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 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 stopAutoRefresh on the CLXBannerAdView immediately 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:

  1. Run the parallel loads, submit the loaded candidates to the arbiter, and attach the winner’s view.
  2. As soon as the winner’s impression fires, immediately start loading a new fill from the winning network.
  3. 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.
  4. Once the outstanding load responses come back, run the arbiter again over the current set of loaded candidates.
  5. 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

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
    ]];

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.