Trusted Arbiter
Compare CloudX bids with supported third-party bids in Flutter apps
Trusted Arbiter compares a loaded CloudX ad with bids from other platforms you mediate yourself and returns the platform to show. Flutter support is backed by the CloudX native SDKs and covers CloudX, AdMob, Google Ad Manager, Unity LevelPlay, PubMatic, and custom publisher-supplied bid inputs.
CloudX.arbiter requires CloudX.initialize to have completed. Build one bid per loaded ad and hand them to it:
import 'package:cloudx_flutter/cloudx.dart';
import 'package:flutter/foundation.dart';
// cloudXAd is the CloudXAd you received in onAdLoaded, passed through unchanged.
final result = await CloudX.arbiter(CloudXArbiterConfiguration(bids: [
CloudXArbiterBid.cloudX(cloudXAd),
CloudXArbiterBid.adMob(adUnitId: adMobAdUnitId),
]));
debugPrint('Selected platform: ${result.platform}');result.platform is a CloudXArbiterPlatform. Compare it against the constants:
if (result.platform == CloudXArbiterPlatform.cloudX) {
// show the CloudX ad
} else if (result.platform == CloudXArbiterPlatform.adMob) {
// show your AdMob ad
}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 still answered from the local fallback, which compares only bids that carry a locally comparable price. A Google bid priced from reported revenue history has no such price, so it loses a comparison against other bids, though it still wins when it is the only bid you supplied. One you priced yourself with manualRevenuePerImpressionUSD competes normally.
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, CloudX.arbiter reaches the service, and a user who taps a button that shows an ad must never wait on a 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 with no network call. Run the cycle again after the ad is hidden, after it fails to display, or when a candidate expires.
CloudXArbiterResult? nextWinner;
int arbiterRound = 0;
// Runs as soon as both candidates have settled, ahead of the placement.
// Only a candidate that actually loaded becomes a bid: bidding an ad unit whose
// load failed can win on its price history and leave you with nothing to show.
Future<void> prepareWinner(CloudXAd? cloudXAd, String? loadedAdMobAdUnitId) async {
/*
* The placement is allowed to show a single candidate and start the next
* cycle while this call is still out, so rounds can overlap. Number each one
* and let only the newest store its result: otherwise an earlier round lands
* last and overwrites the current winner with an ad already shown.
*/
final round = ++arbiterRound;
// A new round means the candidates changed, so whatever the last round left
// here describes ads that are gone. Drop it now rather than let the
// placement show it while this round is still out.
nextWinner = null;
final bids = <CloudXArbiterBid>[
if (cloudXAd != null) CloudXArbiterBid.cloudX(cloudXAd),
if (loadedAdMobAdUnitId != null)
CloudXArbiterBid.adMob(adUnitId: loadedAdMobAdUnitId),
];
if (bids.isEmpty) {
return; // neither side filled; nothing to arbitrate
}
try {
final result =
await CloudX.arbiter(CloudXArbiterConfiguration(bids: bids));
if (round != arbiterRound) {
return; // a newer round started while this one was out; its result wins
}
// A none result is not a winner: storing it would leave you with nothing to
// show and nothing to reload, because both sides still hold their fills.
nextWinner = result.platform == CloudXArbiterPlatform.none ? null : result;
} catch (error) {
// Started from a load callback, so nothing awaits this: an uncaught channel
// failure would escape as an unhandled async error. The loaded ads survive,
// so the next cycle can run the round again.
if (round == arbiterRound) {
nextWinner = null;
}
debugPrint('Arbiter failed: $error');
}
}
// Runs at the placement. No network call here.
void showAd(String cloudXAdUnitId) {
final winner = nextWinner;
nextWinner = null;
/*
* The placement ends this cycle, so retire any round still out. Without this
* a round that started before the placement is still the newest when it
* returns, and it would store a winner naming ads this placement has already
* shown or passed over.
*/
arbiterRound++;
if (winner == null) {
return; // nothing prepared; carry on without an ad
}
if (winner.platform == CloudXArbiterPlatform.cloudX) {
// This cycle prepared an interstitial. A rewarded placement is the same
// flow with showRewarded, and the ad unit id of the rewarded ad.
CloudX.showInterstitial(adUnitId: cloudXAdUnitId);
} else if (winner.platform == CloudXArbiterPlatform.adMob) {
// show your AdMob interstitial
} else {
// This example bids CloudX and AdMob only. Every factory you add needs its
// own branch here, or its win shows nothing at all.
debugPrint('No show path for ${winner.platform}');
}
}If the placement arrives before a winner is stored, either continue without an ad or show the single candidate that did load. That is an acceptable degraded path, never the primary one.
A none round leaves both candidates holding their fills, and holding a fill starts nothing: no ad is shown, so no hidden callback and no display failure arrives to run the next cycle. Take the degraded path there rather than showing nothing, and let that ad’s hidden callback start the cycle again. Showing nothing after none keeps the placement empty until a candidate expires and its replacement load settles.
Destroy the CloudX ad you just showed in its hidden callback, before the next cycle loads. That lets the next load build a fresh instance and run a new auction straight away. Do the same on the other network’s side.
Nothing awaits the round, so catch platform-channel failures where you call the arbiter rather than letting them escape as unhandled async errors. The loaded ads survive a failed round, so the next cycle can simply run it again.
Supported Formats
The arbiter is format-agnostic: it takes any loaded CloudX ad, and the same field mapping applies regardless of format.
- Fullscreen, interstitial and rewarded. Prepare the winner ahead of the placement, as above.
- View, banner and MREC. Nothing user-initiated is waiting, so it is correct to render the winner where
await CloudX.arbiterreturns. See Banner and MREC.
AdMob and Google Ad Manager
CloudX compares a loaded CloudX ad with a loaded AdMob or Ad Manager ad. AdMob and Ad Manager are separate demand sources, so both may bid in the same arbitration.
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. CloudX estimates it from the prior performance of the same ad unit, which is why you do not supply a price and why the revenue reporting below is required.
// networkName is optional; pass the winning ad source when Google exposes it.
final adMobBid = CloudXArbiterBid.adMob(
adUnitId: adMobAdUnitId,
networkName: adMobAdSourceName,
);
// An Ad Manager ad unit id takes the form /NNNNNNN/placement/name.
final adManagerBid = CloudXArbiterBid.gam(
adUnitId: '/21775744923/example/interstitial',
);
final result = await CloudX.arbiter(CloudXArbiterConfiguration(bids: [
CloudXArbiterBid.cloudX(cloudXAd),
adMobBid,
adManagerBid,
]));A winning Google bid reports its own platform, CloudXArbiterPlatform.adMob or CloudXArbiterPlatform.gam, so you do not need to inspect platformName to tell the two apart as you would for a custom bid.
A blank ad unit id still builds a bid rather than crashing your app, but it carries no usable identity: it is never priced and the server rejects it.
Report Google Paid Events Back to CloudX
Reporting Google’s impression-level revenue is a required part of arbitrating AdMob and Ad Manager bids, not optional analytics. An app that takes Google demand without Trusted Arbiter does not need it. Without those events CloudX never learns what Google demand actually paid, and the estimates it supplies to later arbitrations degrade.
Call CloudX.reportRevenueData from your Google paid-event handler. CloudXRevenueData.revenue is a currency-unit amount, and google_mobile_ads reports valueMicros on both platforms, so divide by 1,000,000 on both.
This path assumes the google_mobile_ads package, which wraps the Google Mobile Ads SDK. On Android that pairs with io.cloudx:adapter-googlewaterfall, not io.cloudx:adapter-admob: the two Google SDK generations ship colliding classes and cannot be installed in one app. The package needs Google app identification declared natively whether or not you enable a CloudX Google adapter, so complete the iOS and Android setup entries before you run this flow. An Android app taking Ad Manager demand only declares AD_MANAGER_APP there rather than an AdMob app ID.
import 'package:cloudx_flutter/cloudx.dart';
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
// Attach this to the ad you loaded, passing the platform and the ad unit id
// you gave that bid: CloudXRevenuePlatform.adMob with the AdMob unit, or
// CloudXRevenuePlatform.gam with the Ad Manager unit. The two always travel
// together, so an Ad Manager impression is never reported as AdMob. adFormat
// is the format of this ad, 'banner', 'mrec', 'interstitial' or 'rewarded'.
void reportGooglePaidEvents(
Ad ad,
CloudXRevenuePlatform platform,
String googleAdUnitId,
String adFormat,
) {
ad.onPaidEvent = (paidAd, valueMicros, precision, currencyCode) async {
try {
final accepted = await CloudX.reportRevenueData(CloudXRevenueData(
platform: platform,
revenue: valueMicros / 1000000.0,
adFormat: adFormat,
currencyCode: currencyCode,
precision: _cloudXPrecision(precision),
adUnitId: googleAdUnitId,
));
if (!accepted) {
debugPrint('CloudX dropped the revenue report');
}
} catch (error) {
// Google calls this handler; nothing awaits it, so an uncaught channel
// failure would escape as an unhandled async error.
debugPrint('reportRevenueData failed: $error');
}
};
}
CloudXRevenuePrecision _cloudXPrecision(PrecisionType precision) {
switch (precision) {
case PrecisionType.precise:
return CloudXRevenuePrecision.exact;
case PrecisionType.estimated:
return CloudXRevenuePrecision.estimated;
case PrecisionType.publisherProvided:
return CloudXRevenuePrecision.publisherDefined;
case PrecisionType.unknown:
return CloudXRevenuePrecision.undefined;
}
}Report the ad unit id you passed to the arbiter bid so CloudX attributes the realized price to the right ad unit. reportRevenueData returns false when the data was dropped, for example when the SDK is not initialized, the platform name is blank, or the revenue is not a finite number. See Publisher-Reported Revenue Data for the full field reference.
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 missing currency code counts as USD), so a report of zero revenue is accepted and then ignored for pricing. Google’s test ad units pay zero, which is why AdMob bids never become price-comparable while you test against them.
Manual Input with Pre-Bid ILRD
Some AdMob accounts expose impression-level revenue data pre-bid: the ad value 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. An exact per-impression price known before show is the one case where your own price beats CloudX’s estimate.
final adMobBid = CloudXArbiterBid.adMob(
adUnitId: adMobAdUnitId,
manualRevenuePerImpressionUSD: preBidPricePerImpressionUSD,
);manualRevenuePerImpressionUSD is revenue for a single impression in USD, not a CPM, and a non-USD amount must be converted first. How the value is treated:
0.0is a real price. It means the 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, because a bid with no identity cannot be validated.
The unit you receive depends on the native SDK underneath. See the Android and iOS pages for the per-platform conversion rules for AdMob ad values.
Banner and MREC
Auto-refresh conflicts with arbitration, because a refresh can swap the ad after the arbiter has already picked a winner. Disable auto-refresh in the CloudX dashboard, stop it on the CloudX ad, and disable it on the other arbitrated networks too.
CloudX.createBanner(
adUnitId: adUnitId,
position: CloudXAdViewPosition.bottomCenter,
);
// Creating the ad starts the refresh cycle, so stop it right after.
CloudX.stopBannerAutoRefresh(adUnitId: adUnitId);Show only the winning bid’s ad and keep the rest hidden, using the programmatic overlay API: createBanner or createMrec loads the candidate, and showBanner / hideBanner decide what the user sees after the arbiter returns.
After the winner’s impression, load a new fill from the winning network only, keep the non-winning ads that already hold a fill, re-request just the unfilled networks, and rerun the arbiter once the responses return. Refresh the displayed ad every 20 to 30 seconds; shorter intervals decrease CPM performance.
Other Mediation Platforms
CloudXArbiterBid.levelPlay takes Unity LevelPlay’s own reported values, where precision is LevelPlay’s raw precision token:
final levelPlayBid = CloudXArbiterBid.levelPlay(
networkName: levelPlayAdInfo.adNetwork,
revenue: levelPlayAdInfo.revenue,
precision: levelPlayAdInfo.precision,
);CloudXArbiterBid.pubMatic takes the price from the OpenWrap bid object:
final pubMaticBid = CloudXArbiterBid.pubMatic(
price: pubMaticPrice,
partnerName: pubMaticPartnerName,
);Custom Bid Inputs
Use CloudXArbiterBid.custom to compare CloudX with a platform that has no dedicated factory.
final customBid = CloudXArbiterBid.custom(
platformName: 'my_mediation_platform',
networkName: 'winning_demand_source',
revenuePerImpressionUSD: 0.00125,
precision: CloudXArbiterPrecision.exact,
extras: {'ad_unit': 'third-party-ad-unit-id'},
);
final result = await CloudX.arbiter(CloudXArbiterConfiguration(bids: [
CloudXArbiterBid.cloudX(cloudXAd),
customBid,
]));When a custom bid wins, result.platform is CloudXArbiterPlatform.custom and result.platformName carries the platformName you supplied. Pass revenuePerImpressionUSD as revenue for one impression in USD, not a CPM. CloudXArbiterPrecision offers exact, estimated, publisherDefined, and undefined, and CloudXArbiterPrecision.of accepts a raw token from your own SDK. networkName is required; pass '' when your source does not name a winning network. A custom bid missing a non-blank platformName, revenue, or precision is dropped with a warning and does not compete.
The Result
| Property | Type | Description |
|---|---|---|
platform | CloudXArbiterPlatform | The winning platform, or none. |
platformName | String | The concrete platform name; carries your platformName for a winning custom bid. |
bidId | String? | Identifier of the selected bid, null when there is no winner. |
id | String | Identifier of this arbiter request. |
extras | Map<String, String> | Arbiter-provided metadata for the selected bid. |