Trusted Arbiter
Compare CloudX bids with supported third-party bids in Unity games
Trusted Arbiter compares a loaded CloudX ad with bids from other platforms you mediate yourself and returns the platform to show. Unity SDK 4.1.0 introduced CloudX, Unity LevelPlay and PubMatic bids. Custom bids require 4.4.1 or later, and AdMob and Google Ad Manager bids require 4.5.0 or later.
Why use Trusted Arbiter?
When your app loads ads from several platforms, it needs a way to choose which ad to show. Trusted Arbiter provides a shared comparison API for supported bids.
Less comparison logic to maintain
Submit the supported bids to Trusted Arbiter and use the returned platform to select the ad. Your app continues to manage partner SDK integration, ad loading, and display.
Use available bid values
Trusted Arbiter can compare actual bid values where available. Some demand uses estimated prices. See the pricing guidance below for each supported input.
CloudXSdk.Arbiter requires CloudXSdk.Initialize to have completed. Build one bid per loaded ad and hand them to it:
var bids = new List<CloudXArbiterBid>
{
new CloudXArbiterBid.CloudX(cloudXAd),
new CloudXArbiterBid.AdMob(AdUnitId: adMobAdUnitId),
};
CloudXArbiterResult nextWinner = null;
CloudXSdk.Arbiter(bids, result =>
{
/* Store fullscreen results now and show them later at the placement. */
nextWinner = result.Platform == CloudXArbiterPlatform.None
? null
: result;
});For fullscreen formats, store the result and show it later from the placement callback. Banner and MREC integrations may render the selected view when the arbiter callback returns. The constants are CloudX, AdMob, Gam, LevelPlay, PubMatic, Custom and None.
Trusted Arbiter must be enabled for your app in the CloudX dashboard. Until it is, every call is answered by the local fallback, which compares only bids carrying a locally comparable price. A Google bid without a manual price has no locally comparable price, so it loses against priced bids but still wins when it is the only valid bid. A Google bid with ManualRevenuePerImpressionUSD competes normally.
Demo App
The CloudX Unity demo app runs this page’s cycle for interstitials. CloudX and AdMob load in parallel, both fills become bids, CloudXSdk.Arbiter picks the winner, and the winner is shown from a stored result so the show path makes no network call.
ArbiterInterstitialController.cs
The whole load, arbitrate and show cycle, with both SDKs’ calls in one file. Rewarded follows this file with the rewarded calls substituted.
ArbiterScreen.cs
Brings both SDKs up and creates the controller after CloudX answers. This is demo-only layout; take the rules, not the file.
DemoConfig.cs
App key and ad unit ids per platform. This is the first file to edit when you run the demo yourself.
The demo ships Google’s public AdMob test units, which report revenue of 0.0. CloudX discards zero prices from its pricing history, so the AdMob bid has no comparable historical price while you test. Point the demo at a paying unit to watch prices compete.
When to Run the Arbiter
Run the arbiter when your candidate ads finish loading, never on the show path. With several candidates and the feature enabled, CloudXSdk.Arbiter reaches the service, and a user who reaches an ad placement must never wait for that network call.
Load every candidate in parallel. Once they have all settled, loaded or failed, run the arbiter and store the result. At the placement, show the stored winner immediately. Run the cycle again after the ad is hidden, after it fails to display or when a candidate expires.
private CloudXArbiterResult _nextWinner;
private int _arbiterRound;
/*
* Run this as soon as both candidates have settled, ahead of the placement.
* Only an ad that actually loaded becomes a bid.
*/
private void PrepareWinner(
CloudXAd cloudXAd,
string adMobAdUnitId)
{
var bids = new List<CloudXArbiterBid>();
if (cloudXAd != null && CloudXSdk.IsInterstitialReady(cloudXAd.AdUnitId))
{
bids.Add(new CloudXArbiterBid.CloudX(cloudXAd));
}
if (adMobInterstitial?.CanShowAd() == true)
{
bids.Add(new CloudXArbiterBid.AdMob(AdUnitId: adMobAdUnitId));
}
PrepareWinner(bids);
}
private void PrepareWinner(IReadOnlyList<CloudXArbiterBid> bids)
{
/*
* Rounds can overlap when the placement consumes one candidate while an
* earlier arbitration is still out. Only the newest round may store a
* result, or an old result can point at an ad that has already been shown.
*/
var round = ++_arbiterRound;
_nextWinner = null;
if (bids.Count == 0)
{
BeginLoadCycle();
return;
}
CloudXSdk.Arbiter(bids, result =>
{
if (round != _arbiterRound)
{
return;
}
/* None is not a winner and must not be stored as one. */
_nextWinner = result.Platform == CloudXArbiterPlatform.None
? null
: result;
});
}
/* Run this at the placement. There is no network call here. */
private void ShowAd(string cloudXAdUnitId)
{
var winner = _nextWinner;
_nextWinner = null;
/* Retire any round that still describes this placement's candidates. */
_arbiterRound++;
if (winner == null)
{
BeginLoadCycle();
return;
}
switch (winner.Platform)
{
case CloudXArbiterPlatform.CloudX
when CloudXSdk.IsInterstitialReady(cloudXAdUnitId):
CloudXSdk.ShowInterstitial(cloudXAdUnitId);
break;
case CloudXArbiterPlatform.AdMob
when adMobInterstitial?.CanShowAd() == true:
adMobInterstitial.Show();
break;
default:
Debug.Log($"Winner {winner.Platform} is unsupported or no longer loaded");
BeginLoadCycle();
break;
}
}BeginLoadCycle is your app’s existing cycle entry point: it applies your retry backoff, loads missing candidates in parallel, reuses candidates that still hold a fill, then calls PrepareWinner after they have all settled. If the placement arrives before a winner is stored, continue without an ad while that next cycle runs, or show the single candidate that loaded and let its hidden callback begin the next cycle. That is an acceptable degraded path, never the primary one.
A None round leaves the loaded candidates holding their fills. Holding a fill starts nothing, so no hidden callback or display failure arrives to start the next cycle. The null branch above explicitly starts that next cycle instead of waiting for a callback that will not come.
There is nothing asynchronous to catch around CloudXSdk.Arbiter: it invokes its callback exactly once, and an exception thrown by the callback is caught and logged by the SDK. Google Mobile Ads callbacks are different. Wrap every Google callback body that mutates controller state, calls CloudX, or uses Unity APIs in MobileAdsEventExecutor.ExecuteInUpdate so it runs on the Unity main thread.
Supported Formats
The arbiter is format-agnostic. It accepts any loaded CloudX ad, and the field mapping is the same for every format.
- Fullscreen: interstitial, rewarded and app open. Prepare the winner before the placement, as above.
- View: banner and MREC. Render the winner when the arbiter callback returns. See Banner and MREC.
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 one arbitration.
Google demand does not normally reveal the loaded ad’s price before it is shown. CloudX prices the bid automatically from your historical Google performance, so the bid needs no price. The revenue reporting below feeds that history, which is why it is required. See how AdMob/GAM estimated pricing works.
var adManagerAdUnitId = "/21775744923/example/interstitial";
var adMobNetworkName = "admob";
var bids = new List<CloudXArbiterBid>();
if (cloudXAd != null && CloudXSdk.IsInterstitialReady(cloudXAdUnitId))
{
bids.Add(new CloudXArbiterBid.CloudX(cloudXAd));
}
if (adMobInterstitial?.CanShowAd() == true)
{
var loadedAdapter = adMobInterstitial
.GetResponseInfo()
?.GetLoadedAdapterResponseInfo();
adMobNetworkName = string.IsNullOrWhiteSpace(loadedAdapter?.AdSourceName)
? "admob"
: loadedAdapter.AdSourceName;
bids.Add(new CloudXArbiterBid.AdMob(
AdUnitId: adMobAdUnitId,
NetworkName: adMobNetworkName));
}
if (adManagerInterstitial?.CanShowAd() == true)
{
bids.Add(new CloudXArbiterBid.Gam(
AdUnitId: adManagerAdUnitId));
}
PrepareWinner(bids);
private void ShowGoogleArbitrationWinner(string cloudXAdUnitId)
{
var winner = _nextWinner;
_nextWinner = null;
_arbiterRound++;
switch (winner?.Platform)
{
case CloudXArbiterPlatform.CloudX
when CloudXSdk.IsInterstitialReady(cloudXAdUnitId):
CloudXSdk.ShowInterstitial(cloudXAdUnitId);
break;
case CloudXArbiterPlatform.AdMob
when adMobInterstitial?.CanShowAd() == true:
adMobInterstitial.Show();
break;
case CloudXArbiterPlatform.Gam
when adManagerInterstitial?.CanShowAd() == true:
adManagerInterstitial.Show();
break;
default:
Debug.Log($"Winner {winner?.Platform} is unsupported or no longer loaded");
BeginLoadCycle();
break;
}
}A winning Google bid reports CloudXArbiterPlatform.AdMob or CloudXArbiterPlatform.Gam, so you do not need to inspect PlatformName to distinguish them. A blank ad unit id still builds a bid, but it has no usable identity, is never priced, and is rejected by the server.
Report Google Paid Events Back to CloudX
Reporting Google’s impression-level revenue is required for AdMob and Ad Manager arbitration. It is not optional analytics. Without these events CloudX does not learn what Google demand paid, and later estimates degrade.
This example uses the Google Mobile Ads Unity plugin, com.google.ads.mobile. AdValue.Value is in micros on both platforms, so divide it by 1,000,000. The plugin also requires the AdMob application ID declared natively; complete AdMob Application ID Required. An Android app taking only Ad Manager demand can declare AD_MANAGER_APP instead.
private static CloudXRevenuePrecision ToCloudXRevenuePrecision(
AdValue.PrecisionType precision) =>
precision switch
{
AdValue.PrecisionType.Precise => CloudXRevenuePrecision.Exact,
AdValue.PrecisionType.Estimated => CloudXRevenuePrecision.Estimated,
AdValue.PrecisionType.PublisherProvided => CloudXRevenuePrecision.PublisherDefined,
_ => CloudXRevenuePrecision.Undefined,
};
private static bool ReportGooglePaidEvent(
CloudXRevenuePlatform platform,
AdValue adValue,
string adFormat,
string adUnitId,
string networkName) =>
CloudXSdk.ReportRevenueData(new CloudXRevenueData(
Platform: platform,
Revenue: adValue.Value / 1_000_000.0,
AdFormat: adFormat,
CurrencyCode: adValue.CurrencyCode,
Precision: ToCloudXRevenuePrecision(adValue.Precision),
NetworkName: networkName,
AdUnitId: adUnitId));
adMobInterstitial.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() =>
{
var accepted = ReportGooglePaidEvent(
CloudXRevenuePlatform.AdMob,
adValue,
"interstitial",
adMobAdUnitId,
adMobNetworkName);
Debug.Log($"CloudX accepted revenue: {accepted}");
});
adManagerInterstitial.OnAdPaid += adValue => MobileAdsEventExecutor.ExecuteInUpdate(() =>
{
var accepted = ReportGooglePaidEvent(
CloudXRevenuePlatform.Gam,
adValue,
"interstitial",
adManagerAdUnitId,
"gam");
Debug.Log($"CloudX accepted GAM revenue: {accepted}");
});Report the same ad unit id you passed to the bid so CloudX attributes the realized price correctly. ReportRevenueData returns false when the data is dropped. See Publisher-Reported Revenue Data for every field.
A true return does not mean the price entered CloudX’s history. That history keeps only positive USD amounts carrying an ad unit id. A zero report is accepted and then ignored for pricing, which is why Google’s test units do not make AdMob bids price-comparable.
Manual Input with Pre-Bid ILRD
Some AdMob accounts expose impression-level revenue before the ad is shown. This is a legacy, account-gated capability, so check with your Google account team. An exact per-impression price known before show is the one case where your own price is better than CloudX’s estimate.
var adMobBid = new CloudXArbiterBid.AdMob(
AdUnitId: adMobAdUnitId,
NetworkName: adMobNetworkName,
ManualRevenuePerImpressionUSD: preBidAdValue.Value / 1_000_000.0);ManualRevenuePerImpressionUSD is revenue for one impression in USD, not a CPM. An AdValue.Value of 5000 is 0.005; do not divide by 1,000 again. Convert non-USD values before passing them.
0.0is a real price and means the bid is worth nothing.- Negative and non-finite values are treated as absent and logged.
- A blank ad unit id drops the manual price because the bid cannot be validated.
Banner and MREC
Auto-refresh conflicts with arbitration because it can replace an ad after the arbiter selected the winner. Disable CloudX auto-refresh in the dashboard, stop it before creating the view, and never call StartBannerAutoRefresh. The Google Mobile Ads Unity plugin has no refresh API, so set Automatic refresh to Disabled for the AdMob ad unit in the console.
CloudXSdk.StopBannerAutoRefresh(adUnitId);
CloudXSdk.CreateBanner(adUnitId, new CloudXAdViewConfiguration(CloudXAdViewConfiguration.AdViewPosition.BottomCenter));
CloudXSdk.LoadBanner(adUnitId);Show only the winning bid with ShowBanner and keep the others hidden with HideBanner. Create the Google BannerView, call Hide() before its first LoadAd, and show it only after an AdMob win.
After the winner’s impression, load a new fill from that network only. Keep losing ads that still hold a fill, request only unfilled networks, and rerun the arbiter after all responses return. Refresh the displayed ad every 20 to 30 seconds; shorter intervals reduce CPM performance.
MREC uses the same flow with CreateMrec, LoadMrec, ShowMrec, HideMrec, StopMrecAutoRefresh and the CloudXAdsCallbacks.Mrec callback group.
Other Mediation Platforms
CloudXArbiterBid.LevelPlay takes Unity LevelPlay’s own values. In LevelPlay Unity SDK 8.x, the fields are camelCase and revenue is a nullable double:
var levelPlayBid = new CloudXArbiterBid.LevelPlay(
NetworkName: levelPlayAdInfo.adNetwork,
Revenue: levelPlayAdInfo.revenue ?? 0,
Precision: levelPlayAdInfo.precision);CloudXArbiterBid.PubMatic takes the price from the OpenWrap bid object:
var pubMaticBid = new CloudXArbiterBid.PubMatic(
Price: pubMaticPrice,
PartnerName: pubMaticPartnerName);Custom Bid Inputs
Use CloudXArbiterBid.Custom for a platform that has no dedicated bid type.
var customBid = new CloudXArbiterBid.Custom(
PlatformName: "my_mediation_platform",
NetworkName: "winning_demand_source",
RevenuePerImpressionUSD: 0.00125,
Precision: "EXACT",
Extras: new Dictionary<string, string>
{
["ad_unit"] = "third-party-ad-unit-id",
});When it wins, result.Platform is CloudXArbiterPlatform.Custom and result.PlatformName carries the supplied PlatformName. RevenuePerImpressionUSD is revenue for one impression in USD, not CPM. Precision is a string token such as "EXACT", "ESTIMATED", "PUBLISHER_DEFINED" or "UNDEFINED". A bid missing a non-blank platform name, revenue or precision is dropped and logged.
The Result
| Property | Type | Description |
|---|---|---|
Platform | CloudXArbiterPlatform | The winning platform, or None. |
PlatformName | string | The concrete platform name; carries your platform name for a winning custom bid. |
BidId | string? | Identifier of the selected bid, or null when there is no winner. |
Id | string | Identifier of this arbiter request. |
Extras | IReadOnlyDictionary<string, string> | Arbiter metadata for the selected bid. |