# CloudX Documentation — full text Source: https://docs.cloudx.io/en/introduction Chinese documentation is not included here; append `.md` to any /zh/ URL to read it. --- # Introduction > Welcome to CloudX Docs - maximize your ad revenue with our unified monetization platform > **img:** # CloudX Documentation CloudX is a unified ad monetization platform that helps mobile developers maximize revenue through intelligent mediation and access to premium demand partners. ## Why CloudX? * Higher Revenue Our real-time bidding and mediation technology ensures you get the highest paying ad for every impression. * Easy Integration Get up and running with just a few lines of code — or let your AI coding agent do it on any platform via [AI-assisted integration](/en/ai-integration). * Premium Demand Access demand from Meta, Vungle, and other top-tier ad networks through a single SDK. * Privacy Compliant Built-in support for GDPR, CCPA, and IAB standards (TCF 2.0, GPP, US Privacy). ## Supported Platforms * [Android](/en/android/integration) Native Android SDK with Kotlin and Java support. * [iOS](/en/ios/integration) Native iOS SDK with Swift and Objective-C support. * [React Native](/en/react-native/integration) Cross-platform React Native SDK. Supports iOS and Android. * [Flutter](/en/flutter/integration) Cross-platform Flutter SDK. Supports iOS and Android. * [Unity](/en/unity/integration) Cross-platform Unity plugin for games. Supports Android and iOS builds. --- # How the auction works > How CloudX runs multi-round auctions: line items, priorities, price floors, and round-by-round execution — with interactive simulators. Most monetization platforms run a single auction per impression opportunity. All bidders compete once, under a single set of conditions. CloudX takes a different approach: for every impression, it can run **multiple auction rounds**. Bidders compete head-to-head within each round, while you control which bidders participate in each round and on what terms. ## Why it matters Waterfalls introduced sequencing, but limited competition per impression. Single unified auctions increased competition, but at the expense of publisher control. Multi-round auctions give you the best of both worlds: real-time competition *inside* each round, and publisher-defined sequencing *across* rounds. A single auction where bid price is all that matters has the benefit of simplicity, but that simplicity has costs. When every $60 bid is treated equally — whether the ad is a 120-second non-skippable with a tricky close button or a 30-second skippable with an obvious one — the incentive structure is clear, and ad quality races to the bottom while retention pays the price. Multi-round auctions unlock things a single auction can't express: * **Increased pricing power** — higher-value demand competes at higher floors first; lower-value demand is only accessed if needed. * **Prioritize preferred partners** — give preferred demand partners preferred access, based on creative quality, strategic value, demand diversity, or any other reason. * **Work more flexibly with buyers** — structure access on your terms: PMPs, first-party data sharing, custom floors. The result: more revenue from the same inventory, with the ability to optimize for factors beyond bid price — creative quality, buyer relationships, and user experience. ## Line items define the rules You don't configure rounds directly. You configure [line items](/en/dashboard/line-items), and CloudX groups them into rounds automatically. A line item is a targeting rule paired with bidder configuration: *for requests matching these conditions, invite the specified bidders and reject anything below the stated floor.* | Field | What it does | | ----------- | --------------------------------------------------------------------------------------------- | | Priority | Position in the sequence, from 1 (called first) to 12 (default: 8) | | Targeting | Which requests the line item matches (ad unit, country, OS, connection type, IFA presence, …) | | Bidders | Which demand sources compete when this line item is called | | Price floor | The minimum CPM a bid must meet to win | Every ad request carries attributes — country, OS, language, connection type, session depth, ATT status. A line item participates only when its targeting matches the request: > **AuctionTargeting:** ## Matching line items become rounds Matching line items are grouped by priority, and each distinct priority becomes one round, ordered from priority 1 down. Line items sharing a priority share a round, and the effective floor for a round is the highest across all line items in it: ```text round_floor round_floor = MAX(li.price_floor for each li in round) // This is the common floor on CloudX's ranking basis. // Settlement adjustments can change the floor sent to an individual bidder. ``` > **Warning:** An auction executes **at most 3 rounds**. If more than 3 distinct priorities match a request, CloudX prepares the 3 highest-priority rounds and drops eligible line items from later priorities. Keep your matching line items within 3 priority levels so nothing silently falls out of the auction. > **AuctionRounds:** ## Rounds execute in order Once rounds are formed, CloudX executes them sequentially: 1. **Enter the highest-priority round.** The round with the lowest priority number (closest to 1) runs first. 2. **Send bid requests to all bidders in parallel.** Bidding happens server-side, so every bidder in the round sees the request simultaneously. 3. **Reject anything below the floor.** CloudX applies any settlement adjustment, then discards bids under the round's effective floor. 4. **Select the highest valid bid.** The highest remaining effective bid wins and the auction ends — later rounds are never called. Ties are random. If no valid bid remains, the next round runs. If every round is exhausted (at most 3), the request ends with no fill. Multi-round auctions operate within latency limits set with fill rates and user experience in mind — sequencing across rounds never means bidders waiting on each other inside a round. > **AuctionExecution:** > **Info:** Within a round, competition is a real-time auction — the best effective price wins. Across rounds, *you* decide the order. A direct deal at priority 1 always gets first look, no matter what the open exchange would have paid later. ## Next steps * [Line items](/en/dashboard/line-items) Create line items and configure priorities, floors, targeting, and bidders. * [A/B testing](/en/dashboard/ab-testing) Test different auction configurations against each other. --- # AI-Assisted Integration > Integrate the CloudX SDK using your AI coding agent — Claude Code, Codex, Cursor, or any agent that can read files and fetch URLs Point your AI coding agent at the CloudX agent package and it can carry out the SDK integration for you — on Android, iOS, React Native, Flutter, and Unity. The package contains no version numbers or API snapshots; your agent fetches this documentation live while it works, so its guidance is always current with the latest release. The package also includes playbooks for real-world situations the reference docs don't cover: running CloudX first-look or in parallel alongside an existing mediator (AppLovin MAX, LevelPlay, Google, or another), migrating off a mediation stack, consent and CMP handling, and platform build-system quirks. ## Claude Code Install the plugin once: ```text /plugin marketplace add cloudx-io/cloudx-sdk-agents /plugin install cloudx@cloudx ``` Then, in your app project: ```text integrate CloudX with app key YOUR_APP_KEY ``` ## Cursor / Codex Clone [cloudx-io/cloudx-sdk-agents](https://github.com/cloudx-io/cloudx-sdk-agents) (or vendor its `AGENTS.md`, `.agents/skills/`, and `playbooks/` into your app repository). `AGENTS.md` is the entry point; both tools pick it up natively. ## Any other agent No install needed — paste into any agent with web access: ```text Follow https://raw.githubusercontent.com/cloudx-io/cloudx-sdk-agents/main/.agents/skills/cloudx-integrate/SKILL.md to integrate the CloudX SDK into this project. ``` ## Example prompts * `integrate CloudX with app key YOUR_APP_KEY` — full integration on the detected platform * `we're on AppLovin MAX — integrate CloudX first-look for our banner, MAX stays as fallback` — coexistence with your existing mediation * `add a CloudX banner in parallel with our existing setup so we can compare revenue` — side-by-side comparison * `audit my CloudX integration` — check an existing integration against the current docs > **Info:** Have your CloudX app key and ad unit IDs ready, and tell the agent which > networks are enabled for your app in the CloudX dashboard — it installs > exactly those adapters. ## Related * [CloudX MCP](/en/mcp) — reporting, auction inspection, and documentation search inside the same agent. Complements the integration package. --- # Overview > Overview of CloudX Android SDK setup and core features [![Maven Central](https://img.shields.io/maven-central/v/io.cloudx/sdk)](https://central.sonatype.com/artifact/io.cloudx/sdk) > **Info:** Prefer AI-assisted setup? Point your coding agent at the CloudX agent package — see [AI-assisted integration](/en/ai-integration). ## Installation Requires Android API 23+, Java 8+, and Kotlin 2.1 or newer in the host app. The Magnite adapter needs Kotlin 2.3 or newer; see the note below the dependencies. The CloudX SDK and its adapters are published to Maven Central (`https://repo1.maven.org/maven2/`). Make sure your project resolves from it in `settings.gradle.kts`: ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() // https://repo1.maven.org/maven2/ } } ``` Then add the CloudX SDK to your app's build.gradle: ```kotlin Kotlin DSL dependencies { implementation("io.cloudx:sdk:4.10.0") // Adapters for ad networks implementation("io.cloudx:adapter-bigo:6.1.0.0") // BIGO Ads SDK 6.1.0 implementation("io.cloudx:adapter-digitalturbine:8.4.7.2") // Digital Turbine Marketplace SDK 8.4.7 implementation("io.cloudx:adapter-googlewaterfall:25.5.0.0") // Google Mobile Ads SDK 25.5.0 implementation("io.cloudx:adapter-inmobi:11.5.0.1") // InMobi SDK 11.5.0 implementation("io.cloudx:adapter-magnite:1.0.2.1") // Magnite Ads SDK 1.0.2 implementation("io.cloudx:adapter-meta:6.22.0.1") // Meta Audience Network 6.22.0 implementation("io.cloudx:adapter-mintegral:17.1.81.1") // Mintegral SDK 17.1.81 implementation("io.cloudx:adapter-mobilefuse:1.12.0.1") // MobileFuse SDK 1.12.0 implementation("io.cloudx:adapter-moloco:4.13.0.1") // Moloco SDK 4.13.0 implementation("io.cloudx:adapter-pangle:8.3.0.4.2") // Pangle SDK 8.3.0.4 implementation("io.cloudx:adapter-taurusx:1.19.1.1") // TaurusX SDK 1.19.1 implementation("io.cloudx:adapter-unityads:4.21.0.2") // Unity Ads SDK 4.21.0 implementation("io.cloudx:adapter-verve:3.9.3.0") // Verve HyBid SDK 3.9.3 implementation("io.cloudx:adapter-vungle:7.7.9.1") // Vungle SDK 7.7.9 } ``` ```groovy Groovy dependencies { implementation 'io.cloudx:sdk:4.10.0' // Adapters for ad networks implementation 'io.cloudx:adapter-bigo:6.1.0.0' // BIGO Ads SDK 6.1.0 implementation 'io.cloudx:adapter-digitalturbine:8.4.7.2' // Digital Turbine Marketplace SDK 8.4.7 implementation 'io.cloudx:adapter-googlewaterfall:25.5.0.0' // Google Mobile Ads SDK 25.5.0 implementation 'io.cloudx:adapter-inmobi:11.5.0.1' // InMobi SDK 11.5.0 implementation 'io.cloudx:adapter-magnite:1.0.2.1' // Magnite Ads SDK 1.0.2 implementation 'io.cloudx:adapter-meta:6.22.0.1' // Meta Audience Network 6.22.0 implementation 'io.cloudx:adapter-mintegral:17.1.81.1' // Mintegral SDK 17.1.81 implementation 'io.cloudx:adapter-mobilefuse:1.12.0.1' // MobileFuse SDK 1.12.0 implementation 'io.cloudx:adapter-moloco:4.13.0.1' // Moloco SDK 4.13.0 implementation 'io.cloudx:adapter-pangle:8.3.0.4.2' // Pangle SDK 8.3.0.4 implementation 'io.cloudx:adapter-taurusx:1.19.1.1' // TaurusX SDK 1.19.1 implementation 'io.cloudx:adapter-unityads:4.21.0.2' // Unity Ads SDK 4.21.0 implementation 'io.cloudx:adapter-verve:3.9.3.0' // Verve HyBid SDK 3.9.3 implementation 'io.cloudx:adapter-vungle:7.7.9.1' // Vungle SDK 7.7.9 } ``` > **Note:** Apps that include the Magnite adapter must compile with Kotlin 2.3 or later, because Magnite SDK 1.0.1 and later depend on kotlin-stdlib 2.4.0. ## Initialization ```kotlin Kotlin // Initialize with app key CloudX.initialize( configuration = CloudXInitializationConfiguration.builder("your-app-key-here") .build(), listener = object : CloudXInitializationListener { override fun onInitialized(configuration: CloudXSdkConfiguration) { Log.d("CloudX", "CloudX SDK initialized successfully") } override fun onInitializationFailed(cloudXError: CloudXError) { Log.e("CloudX", "Failed to initialize CloudX SDK: ${cloudXError.message}") } } ) ``` ```java Java // Initialize with app key CloudXInitializationConfiguration configuration = CloudXInitializationConfiguration.builder("your-app-key-here") .build(); CloudX.initialize(configuration, new CloudXInitializationListener() { @Override public void onInitialized(@NonNull CloudXSdkConfiguration configuration) { Log.d("CloudX", "CloudX SDK initialized successfully"); } @Override public void onInitializationFailed(@NonNull CloudXError cloudXError) { Log.e("CloudX", "Failed to initialize CloudX SDK: " + cloudXError.getMessage()); } }); ``` ### App Startup Warm-Up Starting with SDK 4.7.0, the SDK includes an [AndroidX App Startup](https://developer.android.com/topic/libraries/app-startup) initializer that prepares some of its work before you call `CloudX.initialize()`. This is a performance optimization: it lets the SDK initialize and load its first ad faster, without slowing your app's startup. The warm-up does no work on the main thread at process start and waits until your app is in the foreground and idle before doing anything more than lightweight background setup. Processes that never come to the foreground, such as those started for push messages or background jobs, do only that lightweight setup. This does not initialize the SDK. `CloudX.initialize()` is still required, and nothing about when or how you call it changes. The warm-up is best-effort: if it has not finished, the SDK does the work when it needs it, and it cannot cause your app's startup to fail. To turn the warm-up off, remove the initializer in your app manifest: ```xml AndroidManifest.xml ``` The `xmlns:tools` declaration on the provider lets you paste the block in as-is. If your `` element already declares the prefix, you can drop it. Behavior is the same either way. With the initializer removed, the SDK does this preparation when you call `initialize()`, so your first initialization and first ad load are noticeably slower. ## Ad Formats CloudX supports banner, MREC, interstitial, rewarded, native, and App Open ad integrations. Use the format-specific guides for implementation details: * [Banner & MREC ads](/en/android/ad-formats/banner-mrec) Create fixed-size display placements with optional refresh control. * [Interstitial ads](/en/android/ad-formats/interstitial) Load and show full-screen interstitial placements. * [Native ads](/en/android/ad-formats/native) Render native creatives in custom app layouts. * [Rewarded ads](/en/android/ad-formats/rewarded) Reward users after completed rewarded ad views. * [App Open ads](/en/android/ad-formats/app-open) Show fullscreen ads at natural app-open or app-resume moments. ### Ad Information (CloudXAd) The `CloudXAd` object is passed to listener callbacks and contains information about the loaded/displayed ad: | Property | Type | Description | | ------------------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `adFormat` | `CloudXAdFormat` | Ad format (BANNER, MREC, INTERSTITIAL, REWARDED, NATIVE, APP\_OPEN) | | `adUnitId` | `String` | The ad unit ID | | `networkName` | `String` | Name of the winning ad network | | `networkPlacement` | `String?` | Network-specific placement ID | | `mediatorName` | `String` | Mediator that served the ad; `cloudx` for CloudX-won ads | | `mediatorAdUnitId` | `String?` | The mediator's ad unit ID; equals `adUnitId` for CloudX-won ads | | `creativeId` | `String?` | Creative identifier for creative-level issue reporting | | `placement` | `String?` | Custom placement set via `setPlacement()` | | `revenue` | `Double` | Revenue in USD: the bid-time estimate for CloudX bids, or the realized amount when the ad source reports impression-level revenue | | `revenuePrecision` | `String` | Precision of `revenue`: `exact`, `estimated`, `publisher_defined`, or `undefined` | | `adValues` | `Map` | SDK-provided ad metadata, used by features such as Trusted Arbiter | `creativeId` is `null` when the demand source does not report a creative id. It is distinct from the `creativeId` you pass in [`CloudXRevenueData`](#publisher-reported-revenue-data), which describes an impression won outside the CloudX ad flow. ```kotlin override fun onAdLoaded(cloudXAd: CloudXAd) { Log.d("CloudX", "Ad format: ${cloudXAd.adFormat}") Log.d("CloudX", "Network: ${cloudXAd.networkName}") Log.d("CloudX", "Bid-time revenue: ${cloudXAd.revenue}") } ``` ### Error Handling All SDK errors are returned as `CloudXError` objects in listener callbacks: | Property | Type | Description | | ------------------ | ----------------- | ---------------------------------------------------- | | `code` | `CloudXErrorCode` | Error category | | `message` | `String` | Human-readable description | | `cause` | `Throwable?` | Optional underlying exception | | `formattedMessage` | `String` | Pre-formatted message including code and description | #### Error Code Categories | Range | Category | Common Codes | | ------- | -------------- | -------------------------------------------------------------------------------------------- | | 0 | General | `INTERNAL_ERROR` | | 100-199 | Network | `NETWORK_ERROR`, `NETWORK_TIMEOUT`, `NETWORK_SERVER_ERROR`, `NETWORK_NO_CONNECTION` | | 200-299 | Initialization | `NOT_INITIALIZED`, `SDK_DISABLED`, `NO_ADAPTERS_FOUND`, `INVALID_APP_KEY` | | 300-399 | Ad Loading | `NO_FILL`, `INVALID_AD_UNIT`, `ADS_DISABLED` | | 400-499 | Display | `AD_NOT_READY`, `AD_ALREADY_SHOWING` | | 600-699 | Adapter | `ADAPTER_NO_FILL`, `ADAPTER_TIMEOUT`, `ADAPTER_LOAD_TIMEOUT`, `ADAPTER_INITIALIZATION_ERROR` | ## Advanced Features ### Debug Logging ```kotlin Kotlin CloudX.setMinLogLevel(CloudXLogLevel.DEBUG) // Enable debug logging CloudX.setMinLogLevel(CloudXLogLevel.NONE) // Disable all logging ``` ```java Java CloudX.setMinLogLevel(CloudXLogLevel.DEBUG); // Enable debug logging CloudX.setMinLogLevel(CloudXLogLevel.NONE); // Disable all logging ``` **Log Levels:** `VERBOSE` \< `DEBUG` \< `INFO` \< `WARN` \< `ERROR` \< `NONE` Filter logcat with tag `CloudX` to see SDK logs. ### Mediation Debugger Open the Mediation Debugger to check an integration on the device: every installed adapter with its version and status (completed, incomplete, missing, or not configured), the SDK configuration, privacy state, and your ad units. You can also load and show a test ad per ad unit and share the whole report as text. ```kotlin Kotlin CloudX.showMediationDebugger(context) ``` ```java Java CloudX.showMediationDebugger(context); ``` Call it after the `initialize` callback reports success. Before that, it returns `false` and opens nothing. The screen updates live while open. Pass an Activity context where possible. It is meant for development and QA builds; wire it to a debug menu or a gesture. **Note:** A test mode request from the debugger takes effect on the next initialization, after you restart the app. The server decides whether that session runs in test mode. ### Publisher-Reported Revenue Data If your app receives impression-level revenue callbacks or bid metadata from AdMob, InMobi, TopOn, or another mediation platform outside the CloudX ad flow, forward those events to CloudX after initialization: | Field | Required | Description | | ------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `platform` | Yes | `CloudXRevenuePlatform.ADMOB`, `CloudXRevenuePlatform.INMOBI`, `CloudXRevenuePlatform.TOPON`, or `CloudXRevenuePlatform.custom("MyProvider")` | | `revenue` | Yes | Revenue for one impression in the supplied currency, not CPM/eCPM | | `adFormat` | Yes | Ad format string such as `banner`, `mrec`, `interstitial`, `rewarded`, `native`, or `app_open` | | `currencyCode` | No | ISO 4217 currency code, if known | | `precision` | No | `CloudXRevenuePrecision.EXACT`, `ESTIMATED`, `PUBLISHER_DEFINED`, or `UNDEFINED` | | `networkName` | No | Winning ad network name, if known | | `adUnitId` | No | Mediation-platform ad unit ID | | `thirdPartyAdPlacementId` | No | Network-side ad unit or placement ID | | `creativeId` | No | Creative ID from the ad network | | `networkPlacement` | No | Network placement identifier | | `countryCode` | No | User country code, if known | | `userSegment` | No | User segment, if known | `reportRevenueData()` returns `true` when the event is accepted into the CloudX revenue pipeline. It returns `false` if the SDK is not initialized, server-side revenue tracking is disabled, or the platform name is blank. Acceptance is not a delivery guarantee. #### AdMob paid events AdMob Android `AdValue.valueMicros` is reported in micro-units of the supplied currency, so divide it by `1_000_000.0` before passing it to CloudX. ```kotlin private fun Int.toCloudXRevenuePrecision(): CloudXRevenuePrecision = when (this) { AdValue.PrecisionType.PRECISE -> CloudXRevenuePrecision.EXACT AdValue.PrecisionType.ESTIMATED -> CloudXRevenuePrecision.ESTIMATED AdValue.PrecisionType.PUBLISHER_PROVIDED -> CloudXRevenuePrecision.PUBLISHER_DEFINED else -> CloudXRevenuePrecision.UNDEFINED } private fun reportAdMobPaidEvent( adValue: AdValue, adFormat: String, adUnitId: String, responseInfo: ResponseInfo?, ): Boolean { val servedBy = responseInfo?.loadedAdapterResponseInfo return CloudX.reportRevenueData( CloudXRevenueData.builder( platform = CloudXRevenuePlatform.ADMOB, revenue = adValue.valueMicros / 1_000_000.0, adFormat = adFormat, ) .currencyCode(adValue.currencyCode) .precision(adValue.precisionType.toCloudXRevenuePrecision()) .networkName(servedBy?.adSourceName) .adUnitId(adUnitId) .thirdPartyAdPlacementId(servedBy?.adSourceInstanceName) .build(), ) } bannerView.setOnPaidEventListener { adValue -> reportAdMobPaidEvent( adValue = adValue, adFormat = "banner", adUnitId = adUnitId, responseInfo = bannerView.responseInfo, ) } ``` #### InMobi impression events For InMobi, save the `AdMetaInfo` object from `onAdFetchSuccessful`. When `onAdImpression` fires, send the saved `metaInfo.bid` to CloudX, then clear the saved value. ```kotlin private var latestInMobiMetaInfo: AdMetaInfo? = null override fun onAdFetchSuccessful(ad: InMobiBanner, info: AdMetaInfo) { latestInMobiMetaInfo = info } override fun onAdImpression(ad: InMobiBanner) { latestInMobiMetaInfo?.let { metaInfo -> reportInMobiImpression( metaInfo = metaInfo, adFormat = "banner", placementId = inMobiPlacementId.toString(), ) } latestInMobiMetaInfo = null } private fun reportInMobiImpression( metaInfo: AdMetaInfo, adFormat: String, placementId: String, ): Boolean = CloudX.reportRevenueData( CloudXRevenueData.builder( platform = CloudXRevenuePlatform.INMOBI, revenue = metaInfo.bid, adFormat = adFormat, ) .precision(CloudXRevenuePrecision.ESTIMATED) .thirdPartyAdPlacementId(placementId) .creativeId(metaInfo.creativeID) .build(), ) ``` For interstitial and rewarded InMobi ads, use the same pattern with `InterstitialAdEventListener`: save `AdMetaInfo` in `onAdFetchSuccessful`, then report it in `onAdImpression`. #### TopOn revenue events TopOn Android reports revenue in `onAdRevenuePaid(ATAdInfo)`. Set an `ATAdRevenueListener` on the ad object and pass `adInfo.getPublisherRevenue()` directly to CloudX. Do not divide this value like AdMob micros or CPM. ```kotlin private fun reportTopOnRevenue( adInfo: ATAdInfo, adFormat: String, ): Boolean = CloudX.reportRevenueData( CloudXRevenueData.builder( platform = CloudXRevenuePlatform.TOPON, revenue = adInfo.getPublisherRevenue(), adFormat = adFormat, ) .currencyCode(adInfo.getCurrency()) .networkName(adInfo.getNetworkName()) .adUnitId(adInfo.getPlacementId()) .thirdPartyAdPlacementId(adInfo.getNetworkPlacementId()) .networkPlacement(adInfo.getAdsourceId()) .build(), ) mBannerView.setAdRevenueListener(object : ATAdRevenueListener { override fun onAdRevenuePaid(adInfo: ATAdInfo) { reportTopOnRevenue( adInfo = adInfo, adFormat = "banner", ) } }) ``` For other TopOn ad formats, set the same listener on the loaded ad object and pass the matching CloudX `adFormat`. #### Custom platform events Use `custom` for providers that do not have a CloudX SDK constant like AdMob, InMobi, and TopOn do. The value is only the provider name, such as `CloudXRevenuePlatform.custom("TradPlus")` or `CloudXRevenuePlatform.custom("Nimbus")`. Keep the name stable so CloudX can group that provider's revenue consistently. Do not include the amount or currency in the provider name. For a TradPlus impression worth USD 0.01, send `platform = CloudXRevenuePlatform.custom("TradPlus")`, `revenue = 0.01`, and `.currencyCode("USD")`. If your source reports CPM/eCPM, divide by `1_000.0` first. ```kotlin private fun reportCustomRevenueEvent( providerName: String, revenue: Double, adFormat: String, currencyCode: String, adUnitId: String, placementId: String, ): Boolean = CloudX.reportRevenueData( CloudXRevenueData.builder( platform = CloudXRevenuePlatform.custom(providerName), revenue = revenue, adFormat = adFormat, ) .currencyCode(currencyCode) .precision(CloudXRevenuePrecision.PUBLISHER_DEFINED) .adUnitId(adUnitId) .thirdPartyAdPlacementId(placementId) .build(), ) ``` ### Impression-Level Revenue Tracking Set a `revenueListener` on any ad format to receive revenue callbacks. `CloudXAd.revenue` contains the bid-time USD estimate. ```kotlin Kotlin bannerAd.revenueListener = object : CloudXAdRevenueListener { override fun onAdRevenuePaid(cloudXAd: CloudXAd) { Log.d("CloudX", "Bid-time revenue: ${cloudXAd.revenue} from ${cloudXAd.networkName}") } } ``` ```java Java bannerAd.setRevenueListener(new CloudXAdRevenueListener() { @Override public void onAdRevenuePaid(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Bid-time revenue: " + cloudXAd.getRevenue() + " from " + cloudXAd.getNetworkName()); } }); ``` Works with all ad formats (banner, MREC, interstitial, rewarded). > **Info:** Starting with SDK 4.9.0, test-mode sessions deliver per-ad and global revenue callbacks again, including revenue sent through connectors, with `revenue` set to 0 and `revenuePrecision` set to `"undefined"`. SDK 4.7.0 to 4.8.x suppressed them. Use these callbacks to check your revenue wiring, not the amounts. ### Test Mode Test mode is **server-controlled** via device whitelisting. This provides better security and control over which devices receive test ads. **To enable test mode:** 1. Initialize the SDK and check logcat for your device advertising ID: ``` [CloudX][AdvertisingIdProvider] Device IFA for test whitelisting: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (LAT: false) ``` 2. Copy the advertising ID and add it to your device whitelist on the CloudX server dashboard 3. The SDK will automatically configure adapters for test mode and include the test flag in bid requests **Note:** Test mode is determined by the server, so you don't need to change any code between development and production builds. ### Privacy Compliance The CloudX SDK supports GDPR and CCPA privacy compliance by reading standard IAB privacy strings from SharedPreferences. These values are typically set automatically by your Consent Management Platform (CMP) such as Google UMP, OneTrust, or Sourcepoint. #### How It Works The SDK automatically detects user location and reads consent signals: 1. **EU Users (GDPR)**: Checks TCF v2 consent for purposes 1 and 2 per the [IAB Global Vendor List](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework) and vendor consent ([CloudX Vendor ID: 1510](https://vendor-list.consensu.org/v3/vendor-list.json)) 2. **US Users (CCPA)**: Checks for sale/sharing opt-out signals 3. **Other Regions**: No restrictions applied When consent is denied or user opts out, the SDK removes PII from ad requests: * Advertising ID (GAID) is cleared * Geo coordinates (lat/lon) are removed * User key-values are not sent * Hashed user ID is removed from bidder-bound requests and suppressed from impression reporting #### Supported Privacy Keys | Key | Standard | Description | | ---------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `IABGPP_HDR_GppString` | [GPP](https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform) | Global Privacy Platform string (modern) | | `IABGPP_GppSID` | GPP | Section IDs (e.g., "2" for EU, "7" for US-National, "8" for US-CA) | | `IABTCF_TCString` | [TCF v2](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework) | GDPR consent string (legacy) | | `IABTCF_gdprApplies` | TCF v2 | Whether GDPR applies (1 = yes, 0 = no) | | `IABUSPrivacy_String` | [US Privacy](https://github.com/InteractiveAdvertisingBureau/USPrivacy) | CCPA privacy string (legacy, e.g., "1YNN") | > **Note**: The SDK prioritizes GPP (modern standard) over legacy TCF/US Privacy strings when both are available. #### Manual Privacy API If you manage user consent yourself (without a CMP), you can set GDPR and CCPA privacy status directly. These must be called prior to SDK initialization. ```kotlin Kotlin // GDPR consent: true, false, or null to defer to CMP CloudX.setHasUserConsent(true) // CCPA do-not-sell: true, false, or null to defer to CMP CloudX.setDoNotSell(true) ``` ```java Java // GDPR consent: true, false, or null to defer to CMP CloudX.setHasUserConsent(true); // CCPA do-not-sell: true, false, or null to defer to CMP CloudX.setDoNotSell(true); ``` > **Note:** When both manual values and CMP signals are present, CMP signals (GPP/TCF/US Privacy) take priority. Manual values act as a fallback when no CMP is integrated. ### User Targeting ```kotlin Kotlin // Set hashed user ID for targeting CloudX.setHashedUserId("hashed-user-id") // Set custom user key-value pairs CloudX.setUserKeyValue("age", "25") CloudX.setUserKeyValue("gender", "male") CloudX.setUserKeyValue("location", "US") // Set custom app key-value pairs CloudX.setAppKeyValue("app_version", "1.0.0") CloudX.setAppKeyValue("user_level", "premium") // Clear all custom key-values CloudX.clearAllKeyValues() ``` ```java Java // Set hashed user ID for targeting CloudX.setHashedUserId("hashed-user-id"); // Set custom user key-value pairs CloudX.setUserKeyValue("age", "25"); CloudX.setUserKeyValue("gender", "male"); CloudX.setUserKeyValue("location", "US"); // Set custom app key-value pairs CloudX.setAppKeyValue("app_version", "1.0.0"); CloudX.setAppKeyValue("user_level", "premium"); // Clear all custom key-values CloudX.clearAllKeyValues(); ``` The hashed user ID is a publisher-provided pseudonymous identifier. When applicable privacy signals allow it, CloudX captures the value at auction time and returns it as `hashed_user_id` in the [request](/en/cli/exports#cloudx-export-requests) and [impression](/en/cli/exports#cloudx-export-impressions) activity exports. Use `auction_id` to join the exports. Use the ID to join CloudX activity to cohorts in your own user data. The export value is empty when no ID was set, persistence was suppressed by privacy signals, or the value exceeded 128 characters. Do not pass raw personal data. User and app key-values are also available in the [request activity export](/en/cli/exports#cloudx-export-requests). Use `setUserKeyValue` for session-wide user attributes and `setAppKeyValue` for session-wide app attributes. The SDK sends current values with each later auction request. #### Identity passthrough Pass UID 2.0, EUID, LiveRamp, ID5, and Intent IQ with `setUserKeyValue`. Set each value when you have it, and again when it refreshes. | Key | Pass this | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `uidapi.com` | UID2 advertising token, not the refresh token. Do not decrypt it. [UID2 docs](https://unifiedid.com/docs/ref-info/ref-tokens) | | `euid.eu` | EUID advertising token, not the refresh token. Do not decrypt it. [EUID docs](https://euid.eu/docs/ref-info/ref-tokens) | | `liveramp.com` | LiveRamp ATS envelope, not a RampID. [LiveRamp docs](https://developers.liveramp.com/authenticatedtraffic-api/docs/pass-mobile-envelopes-to-the-bidstream) | | `id5-sync.com` | ID5 universal UID. Do not pass `0`. [ID5 docs](https://wiki.id5.io/docs/mobile-in-app-integration) | | `intentiq.com` | Intent IQ ID (IIQ ID) from your Intent IQ integration. [Intent IQ docs](https://docs.prebid.org/dev-docs/modules/userid-submodules/intentiq.html) | CloudX does not generate these IDs. Create them with UID2, EUID, LiveRamp ATS, ID5, or Intent IQ, then pass the string. This is separate from hashed user ID. ```kotlin Kotlin CloudX.setUserKeyValue("uidapi.com", uid2Token) CloudX.setUserKeyValue("euid.eu", euidToken) CloudX.setUserKeyValue("liveramp.com", liveRampEnvelope) CloudX.setUserKeyValue("id5-sync.com", id5Id) CloudX.setUserKeyValue("intentiq.com", intentIQId) ``` ```java Java CloudX.setUserKeyValue("uidapi.com", uid2Token); CloudX.setUserKeyValue("euid.eu", euidToken); CloudX.setUserKeyValue("liveramp.com", liveRampEnvelope); CloudX.setUserKeyValue("id5-sync.com", id5Id); CloudX.setUserKeyValue("intentiq.com", intentIQId); ``` > **Warning:** Use `setUserKeyValue` only. Do not put these tokens in `setAppKeyValue` or `setExtraParameter`. Extra parameters apply to one ad load. Key names must match exactly. > **Note:** Do not set a key until you have a value. Setting it again replaces the previous value. If a value expires without a replacement, set that key to an empty string to stop sending its EID. `clearAllKeyValues()` clears all user and app key-values and the hashed user ID. For metadata that belongs to one ad object or load, call `setExtraParameter` before `load`: ```kotlin adView.setExtraParameter("requestId", "request-456") adView.setExtraParameter("impressionKey", "impression-789") adView.load() ``` Extra parameters remain on that ad object until changed or cleared. The SDK snapshots them for each load. CloudX returns the complete bag in the `extra_parameters` request-export column. The reserved `tags` key also controls [tag-based routing](/en/dashboard/tags); use another key for correlation metadata. CloudX stores each of the three compact JSON bags only when the entire bag is at most 256 UTF-8 bytes. The limit is independent for each export column and includes all combined keys, values, quotes, separators, and braces—not 256 bytes per K/V pair. When privacy rules prohibit publisher-data persistence, or a bag is malformed or too large, its export cell is empty. Do not include raw personal data, secrets, or consent strings. ## Support For support, contact [support@cloudx.io](mailto:support@cloudx.io) --- # Sample App > Run the public CloudX Android demo app and point it at your own app The [cloudx-io/cloudx-android-public](https://github.com/cloudx-io/cloudx-android-public) repository is a runnable sample app. It opens on a screen that picks one of three flows. General shows banner, MREC, interstitial, rewarded, app open and native ads, each in one self-contained class under `app/src/main/java/io/cloudx/demo/demoapp/ads/`. First Look loads CloudX first and falls back to AdMob, as described in [First Look](/en/android/integrations/first-look). Arbiter/TPA loads CloudX and AdMob in parallel and lets [Trusted Arbiter](/en/android/trusted-arbiter) pick the one to show. It ships with CloudX demo dashboard IDs and Google's AdMob test IDs, so it runs without an account: `./gradlew :app:installDebug` installs it. ## Use your own CloudX app To point it at your own CloudX app, replace the app key and ad unit IDs in `DemoConfig.kt`, then set `applicationId` in `app/build.gradle.kts` to the package name registered for that app. Bid requests are authorized per app key and package name, so both must match your dashboard app or the SDK gets no fill. --- # First Look > Give CloudX the first chance to fill an ad placement, then fall back to your existing mediation setup First Look gives CloudX the first chance to fill a placement while preserving your existing mediation setup as the fallback path. Start with one placement, verify load and show behavior, then expand to more placements. This example uses an AdMob interstitial fallback. The same pattern applies to other fallback mediators: load CloudX first, load the fallback only if CloudX does not load, and keep the app flow moving when neither source is ready. > **Info:** Initialize CloudX and Google Mobile Ads before creating the controller. Keep your existing pacing, retry, and placement timing rules around this controller. ```kotlin FirstLookInterstitialController.kt import android.app.Activity import com.google.android.gms.ads.AdError import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.FullScreenContentCallback import com.google.android.gms.ads.LoadAdError import com.google.android.gms.ads.interstitial.InterstitialAd import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback import io.cloudx.sdk.CloudX import io.cloudx.sdk.CloudXAd import io.cloudx.sdk.CloudXError import io.cloudx.sdk.CloudXInterstitialAd import io.cloudx.sdk.CloudXInterstitialListener class FirstLookInterstitialController( private val activity: Activity, private val cloudXAdUnitId: String, private val adMobAdUnitId: String, ) { enum class Source { CLOUDX, ADMOB, } interface Listener { fun onAdLoaded(source: Source) {} fun onAdLoadFailed(source: Source, message: String?) {} fun onAdShown(source: Source) {} fun onAdShowFailed(source: Source, message: String?) {} fun onAdClosed(source: Source) {} fun onAdClicked(source: Source) {} } var listener: Listener? = null private val cloudXInterstitial: CloudXInterstitialAd = CloudX.createInterstitial(activity, cloudXAdUnitId) private var adMobInterstitial: InterstitialAd? = null private var isLoadingCloudX = false private var isLoadingAdMob = false init { cloudXInterstitial.listener = createCloudXListener() } fun load() { if (isLoadingCloudX || isLoadingAdMob || cloudXInterstitial.isAdReady || adMobInterstitial != null) { return } isLoadingCloudX = true cloudXInterstitial.load() } fun show(): Boolean { if (cloudXInterstitial.isAdReady) { cloudXInterstitial.show(activity) return true } return showAdMobFallback() } fun destroy() { cloudXInterstitial.destroy() adMobInterstitial = null listener = null } private fun createCloudXListener() = object : CloudXInterstitialListener { override fun onAdLoaded(cloudXAd: CloudXAd) { isLoadingCloudX = false listener?.onAdLoaded(Source.CLOUDX) } override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) { isLoadingCloudX = false loadAdMobFallback() } override fun onAdDisplayed(cloudXAd: CloudXAd) { listener?.onAdShown(Source.CLOUDX) } override fun onAdDisplayFailed(cloudXAd: CloudXAd, cloudXError: CloudXError) { if (showAdMobFallback()) { return } listener?.onAdShowFailed(Source.CLOUDX, cloudXError.message) } override fun onAdHidden(cloudXAd: CloudXAd) { listener?.onAdClosed(Source.CLOUDX) } override fun onAdClicked(cloudXAd: CloudXAd) { listener?.onAdClicked(Source.CLOUDX) } } private fun loadAdMobFallback() { if (isLoadingAdMob || adMobInterstitial != null) { return } isLoadingAdMob = true InterstitialAd.load( activity, adMobAdUnitId, AdRequest.Builder().build(), object : InterstitialAdLoadCallback() { override fun onAdLoaded(interstitialAd: InterstitialAd) { isLoadingAdMob = false adMobInterstitial = interstitialAd.apply { fullScreenContentCallback = createAdMobFullScreenCallback() } listener?.onAdLoaded(Source.ADMOB) } override fun onAdFailedToLoad(loadAdError: LoadAdError) { isLoadingAdMob = false adMobInterstitial = null listener?.onAdLoadFailed(Source.ADMOB, loadAdError.message) } }, ) } private fun showAdMobFallback(): Boolean { val ad = adMobInterstitial ?: return false ad.show(activity) return true } private fun createAdMobFullScreenCallback() = object : FullScreenContentCallback() { override fun onAdShowedFullScreenContent() { adMobInterstitial = null listener?.onAdShown(Source.ADMOB) } override fun onAdDismissedFullScreenContent() { adMobInterstitial = null listener?.onAdClosed(Source.ADMOB) } override fun onAdFailedToShowFullScreenContent(adError: AdError) { adMobInterstitial = null listener?.onAdShowFailed(Source.ADMOB, adError.message) } override fun onAdClicked() { listener?.onAdClicked(Source.ADMOB) } } } ``` Use `show()` at the placement moment. If it returns `false`, neither CloudX nor the fallback had a ready ad, so continue the app flow without showing an ad. ```kotlin if (!firstLookInterstitial.show()) { continueToNextScreen() } ``` After a show, close, or terminal failure, call `load()` again when your app is ready to prepare the next placement opportunity. > **Tip:** This sample keeps the fallback lazy: AdMob loads only after CloudX fails to load. Do not parallel-load both sources unless CloudX recommends a different rollout strategy for your app. ## Demo App The [CloudX Android sample app](https://github.com/cloudx-io/cloudx-android-public) runs this flow on device. It structures the flow as three files, the controller and one source per SDK, and its host screen retries a failed load or show with a 2 to 60 second backoff. * [FirstLookInterstitialController.kt](https://github.com/cloudx-io/cloudx-android-public/blob/main/app/src/main/java/io/cloudx/demo/demoapp/ads/firstlook/FirstLookInterstitialController.kt) Gives CloudX the first load attempt. AdMob loads only after CloudX fails or is unavailable. * [CloudXFirstLookSource.kt](https://github.com/cloudx-io/cloudx-android-public/blob/main/app/src/main/java/io/cloudx/demo/demoapp/ads/firstlook/CloudXFirstLookSource.kt) CloudX's leg of the flow. * [AdMobFirstLookSource.kt](https://github.com/cloudx-io/cloudx-android-public/blob/main/app/src/main/java/io/cloudx/demo/demoapp/ads/firstlook/AdMobFirstLookSource.kt) The lazy AdMob fallback. A loaded ad shows once and expires after one hour. * [FirstLookActivity.kt](https://github.com/cloudx-io/cloudx-android-public/blob/main/app/src/main/java/io/cloudx/demo/demoapp/ui/FirstLookActivity.kt) Waits for both SDKs, retries with backoff and reports each step in the status lines. This is demo-only layout; take the rules, not the file. Copy the first three together. The two sources log through the demo's `DemoLog`; swap in your own logging when you copy them. --- # Trusted Arbiter > Compare CloudX bids with supported third-party bids in Android apps Trusted Arbiter compares a loaded CloudX bid with supported third-party bids and returns the selected platform. CloudX SDK versions 4.1.0 and later support CloudX, Unity LevelPlay, and PubMatic bid inputs. CloudX SDK 4.2.0 and later also supports custom publisher-supplied bid inputs. ## 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. > **Info:** Call `CloudX.arbiter()` after `CloudX.initialize()` has completed. Until the arbiter service is available, the SDK falls back to selecting the highest comparable USD bid from the supplied inputs. ## Demo App The [CloudX Android sample app](https://github.com/cloudx-io/cloudx-android-public) runs this page's cycle for interstitials. CloudX and AdMob load in parallel, the loaded ads become bids, `CloudX.arbiter()` picks the winner, and the winner is shown from a stored result so the show path makes no network call. * [ArbiterInterstitialController.kt](https://github.com/cloudx-io/cloudx-android-public/blob/main/app/src/main/java/io/cloudx/demo/demoapp/ads/arbiter/ArbiterInterstitialController.kt) The whole load, arbitrate and show cycle, with both SDKs' calls and the AdMob revenue report in one file. Rewarded follows this file with the rewarded calls substituted. * [ArbiterActivity.kt](https://github.com/cloudx-io/cloudx-android-public/blob/main/app/src/main/java/io/cloudx/demo/demoapp/ui/ArbiterActivity.kt) Brings up both SDKs, waits up to 15 seconds for CloudX, retries with a 2 to 60 second backoff and wires the Show button. This is demo-only layout; take the rules, not the file. * [DemoConfig.kt](https://github.com/cloudx-io/cloudx-android-public/blob/main/app/src/main/java/io/cloudx/demo/demoapp/DemoConfig.kt) App key and ad unit IDs. This is the first file to edit when you run the demo yourself. The controller logs through the demo's `DemoLog`; swap in your own logging when you copy it. The demo ships Google's AdMob test units, which report revenue of 0. CloudX does not keep a price of 0, so the AdMob bid has no comparable price while you test. Point the demo at a paying unit to watch prices compete, or give the AdMob bid a manual price for one launch as the demo's README shows. ## When to run the arbiter Run the arbiter when the candidate ads finish loading — never on the show path. The arbiter call is a network round trip, so a user who taps a button that shows an ad must never be left waiting on it. Arbitrate ahead of the placement and the decision is already made when the placement arrives. ### Fullscreen formats (interstitial, rewarded, app open): prepare ahead 1. Load all candidates in parallel. 2. When every candidate has settled — loaded or failed — run the arbiter and **store** the result. 3. At the placement, show the stored winner immediately. No arbiter call here. 4. Start the cycle again once the ad has been shown and dismissed, fails to display, or a candidate expires. ```kotlin private var nextWinner: CloudXArbiterResult? = null // Runs as soon as the candidates have loaded, ahead of the placement. private fun prepareWinner() { val configuration = CloudXArbiterConfiguration.builder(bids).build() CloudX.arbiter(configuration, object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { nextWinner = result } }) } // Runs at the placement. No network call here. fun showInterstitial(activity: Activity) { when (nextWinner?.platform) { CloudXArbiterPlatform.CLOUDX -> cloudXInterstitial.show(activity) CloudXArbiterPlatform.LEVELPLAY -> levelPlayInterstitial.showAd(activity) else -> { } // no winner prepared; continue without an ad } nextWinner = null } ``` If the placement arrives before a winner is stored, either continue the app flow without an ad or show the single candidate that did load. Showing a lone candidate is an acceptable degraded path, but it must never be the primary one — an arbitrated winner earns more than an unarbitrated fill. ### View formats (banner, MREC, native): arbitrate, then render View formats have no user-initiated show, so nothing is waiting on the round trip. Here it is correct to attach or render the winner directly inside the completion callback: the arbiter completing *is* the trigger to show. Only the winner's view or assets may ever be attached — see [Banner and MREC arbitration](#banner-and-mrec-arbitration) for the attachment and refresh rules. > **Note:** The arbiter call always completes. Its per-call timeout is supplied by the server in your init configuration, and on timeout, error, or an unavailable arbiter service the SDK returns a local fallback: the highest comparable USD bid among the inputs you supplied. A single supplied bid wins immediately without any service call. Do not wrap `CloudX.arbiter()` in a timeout of your own and do not compare revenue yourself — the SDK owns both the timeout and the fallback, and your own timer can only discard a decision that was on its way. > > `CloudXArbiterPlatform.NONE` means no winner was selected. With no bids supplied that is the only possible answer; with several bids it means the fallback found no candidate carrying a locally comparable price — for example Google bids without `manualRevenuePerImpressionUSD`. ## Supported ad formats Trusted Arbiter is format-agnostic: arbiter payloads are attached to CloudX ads of every format, and any loaded CloudX ad can be compared against the supplied third-party bids. * **Interstitial, rewarded, and app open** (fullscreen formats) prepare a winner ahead of the placement — see [When to run the arbiter](#when-to-run-the-arbiter) — and follow the step-by-step walkthrough and controller pattern shown later on this page. Worked examples: [Interstitial example](#interstitial-example) and [App Open example](#app-open-example). Rewarded follows the interstitial pattern unchanged — only the ad type differs. * **Banner, MREC, and native** (view formats) arbitrate and then render. They need the additional handling covered in [Banner and MREC arbitration](#banner-and-mrec-arbitration) below, since a losing bid's view must never be attached to the view hierarchy. Worked examples: [Banner example](#banner-example) and [Native example](#native-example). ## Basic API Create bid candidates from loaded ads, then pass them to `CloudX.arbiter()`. ```kotlin // cloudXAd is the CloudXAd from a CloudX onAdLoaded callback. // levelPlayAdInfo is the Unity LevelPlay ad info object. // pobBid is the PubMatic/OpenWrap bid object. val bids = listOf( CloudXArbiterBid.cloudX(cloudXAd), CloudXArbiterBid.levelPlay( networkName = levelPlayAdInfo.adNetwork, revenue = levelPlayAdInfo.revenue, precision = levelPlayAdInfo.precision, ), CloudXArbiterBid.pubmatic( price = pobBid.price, partnerName = pobBid.partnerName, ) ) val configuration = CloudXArbiterConfiguration.builder(bids).build() CloudX.arbiter(configuration, object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { Log.d("CloudX", "Selected platform: ${result.platform.name}") } }) ``` `CloudXArbiterBid.cloudX()` accepts the `CloudXAd` object from a CloudX load callback. `levelPlay()` accepts Unity LevelPlay ad info values. `pubmatic()` accepts a PubMatic OpenWrap bid price and optional partner name. `partnerName` and the `extras` map are optional on the bid factories. `onCompleted()` runs on the main thread. For fullscreen formats, store the result there and show it at the placement — see [When to run the arbiter](#when-to-run-the-arbiter); view formats may render the winner directly from it. `result.platform` is `CloudXArbiterPlatform.CLOUDX`, `LEVELPLAY`, or `PUBMATIC` for the selected platform, or `CloudXArbiterPlatform.NONE` when no winner could be selected — either because no bids were supplied, or because the fallback found no candidate carrying a locally comparable price. > **Tip:** Kotlin callers can use the suspending overload instead of a listener. It returns the same `CloudXArbiterResult`: > > ```kotlin > val result = CloudX.arbiter(configuration) > Log.d("CloudX", "Selected platform: ${result.platform.name}") > ``` ## 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 prices the bid automatically from your historical Google performance, so you do not supply a price and no pre-bid pricing API is required. The revenue reporting below feeds that history. See [how AdMob/GAM estimated pricing works](/en/networks/google-estimated). 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](#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. After you show an AdMob or Ad Manager ad that won an arbitration, hand Google's impression-level revenue for that ad to the CloudX SDK. Without it, CloudX never learns what Google demand actually paid, and the estimates it produces for future arbitrations degrade. Google delivers this revenue per ad through `OnPaidEventListener` — there is no global impression bus — so attach the listener to every arbitrated Google ad and forward each event through `CloudX.reportRevenueData()`. `AdValue.valueMicros` is in micro-units of the reported currency, so divide by `1_000_000.0` to get revenue for one impression. Use `CloudXRevenuePlatform.ADMOB` for AdMob ads and `CloudXRevenuePlatform.GAM` for Ad Manager ads. ```kotlin private fun Int.toCloudXRevenuePrecision(): CloudXRevenuePrecision = when (this) { AdValue.PrecisionType.PRECISE -> CloudXRevenuePrecision.EXACT AdValue.PrecisionType.ESTIMATED -> CloudXRevenuePrecision.ESTIMATED AdValue.PrecisionType.PUBLISHER_PROVIDED -> CloudXRevenuePrecision.PUBLISHER_DEFINED else -> CloudXRevenuePrecision.UNDEFINED } private fun reportGooglePaidEvent( platform: CloudXRevenuePlatform, adValue: AdValue, adFormat: String, adUnitId: String, responseInfo: ResponseInfo?, ): Boolean { val servedBy = responseInfo?.loadedAdapterResponseInfo return CloudX.reportRevenueData( CloudXRevenueData.builder( platform = platform, revenue = adValue.valueMicros / 1_000_000.0, adFormat = adFormat, ) .currencyCode(adValue.currencyCode) .precision(adValue.precisionType.toCloudXRevenuePrecision()) .networkName(servedBy?.adSourceName) .adUnitId(adUnitId) .thirdPartyAdPlacementId(servedBy?.adSourceInstanceName) .build(), ) } // Attach the listener to the arbitrated Google ad before you show it. adMobInterstitial.setOnPaidEventListener { adValue -> reportGooglePaidEvent( platform = CloudXRevenuePlatform.ADMOB, adValue = adValue, adFormat = "interstitial", adUnitId = adMobAdUnitId, responseInfo = adMobInterstitial.responseInfo, ) } // An Ad Manager ad reports the same way, with the GAM platform. adManagerInterstitial.setOnPaidEventListener { adValue -> reportGooglePaidEvent( platform = CloudXRevenuePlatform.GAM, adValue = adValue, adFormat = "interstitial", adUnitId = adManagerAdUnitId, responseInfo = adManagerInterstitial.responseInfo, ) } ``` `reportRevenueData()` returns `true` when the event is accepted into the CloudX revenue pipeline, and `false` if the SDK is not initialized or server-side revenue tracking is disabled. For banner and MREC, attach the listener once to the `AdView` — Google re-fires paid events on the same view — and pass `"banner"` or `"mrec"` as the format. See [Publisher-Reported Revenue Data](/en/android/integration#publisher-reported-revenue-data) for the full field reference and the other supported platforms. Create the bid from the ad unit id of the ad you loaded: ```kotlin // networkName is optional; pass the winning ad source when you know it, e.g. // responseInfo?.loadedAdapterResponseInfo?.adSourceName. val adMobBid = CloudXArbiterBid.adMob( adUnitId = adMobAdUnitId, networkName = adMobNetworkName ?: "admob", manualRevenuePerImpressionUSD = null, extras = emptyMap(), ) // An Ad Manager ad unit id takes the form /NNNNNNN/placement/name. val adManagerBid = CloudXArbiterBid.gam( adUnitId = "/21775744923/example/interstitial", networkName = "gam", manualRevenuePerImpressionUSD = null, extras = emptyMap(), ) val configuration = CloudXArbiterConfiguration.builder( listOf(CloudXArbiterBid.cloudX(cloudXAd), adMobBid, adManagerBid) ).build() // Runs once the candidates have loaded, ahead of the placement. Store the winner. CloudX.arbiter(configuration, object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { nextWinner = result } }) // Runs at the placement. No arbiter call here. fun showInterstitial(activity: Activity) { when (nextWinner?.platform) { CloudXArbiterPlatform.CLOUDX -> cloudXInterstitial.show(activity) CloudXArbiterPlatform.ADMOB -> adMobInterstitial.show(activity) CloudXArbiterPlatform.GAM -> adManagerInterstitial.show(activity) else -> { } // no winner prepared; continue without an ad } nextWinner = null } ``` A winning Google bid reports its own platform — `CloudXArbiterPlatform.ADMOB` or `CloudXArbiterPlatform.GAM`. You no longer need to inspect `result.platformName` to tell these sources apart, as you would for a [custom bid](#custom-bid-inputs). > **Note:** Google's paid-event callback fires after an impression, so it cannot price the same loaded ad before the arbiter runs. Post-impression revenue is exactly what you forward through `CloudX.reportRevenueData()`. 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 ad value 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. > **Warning:** We recommend manually assigned prices only when they come from pre-bid ILRD. Prices from weaker sources — average eCPMs, guesses, or post-impression paid events — degrade arbitration rather than improving it. Leave `manualRevenuePerImpressionUSD` null or absent in those cases and let CloudX's estimate price the bid instead. Pass the pre-bid ad value as `manualRevenuePerImpressionUSD` and it overrides the estimate: ```kotlin val adMobBid = CloudXArbiterBid.adMob( adUnitId = adMobAdUnitId, networkName = adMobNetworkName ?: "admob", // valueMicros is in micros: 1,000,000 micros is one currency unit. // Do not also divide by 1,000 — this is already a per-impression value, not a CPM. manualRevenuePerImpressionUSD = preBidAdValue.valueMicros / 1_000_000.0, extras = emptyMap(), ) ``` 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: * **A Google ad value** is reported in **micros** on Android, so divide by 1,000,000. A `valueMicros` of `5000` is `0.005` per impression. Do not confuse a micros value with an eCPM and divide by 1,000 as well. * **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 `CloudX.arbiter()`. It uses an interstitial, but the same field mapping applies to any format — see [Supported ad formats](#supported-ad-formats) for what changes for Banner and MREC. 1. **Load both candidates.** Create the CloudX and LevelPlay interstitials, attach listeners, and start a load on each platform. ```kotlin val cloudXInterstitial = CloudX.createInterstitial(context, "YOUR_CLOUDX_AD_UNIT_ID") cloudXInterstitial.listener = cloudXListener cloudXInterstitial.load() val levelPlayInterstitial = LevelPlayInterstitialAd("YOUR_LEVELPLAY_AD_UNIT_ID") levelPlayInterstitial.setListener(levelPlayListener) levelPlayInterstitial.loadAd() ``` 2. **Capture each platform's loaded ad.** LevelPlay delivers a `LevelPlayAdInfo` in its `onAdLoaded` callback; CloudX delivers a `CloudXAd` in `onAdLoaded`. Hold onto both — you read the arbiter inputs from them in the next step. ```kotlin private var cloudXAd: CloudXAd? = null private var levelPlayInfo: LevelPlayAdInfo? = null // CloudXInterstitialListener override fun onAdLoaded(cloudXAd: CloudXAd) { this.cloudXAd = cloudXAd } // LevelPlayInterstitialAdListener override fun onAdLoaded(levelPlayAdInfo: LevelPlayAdInfo) { levelPlayInfo = levelPlayAdInfo } ``` 3. **Map the values into bids.** Read the LevelPlay fields off `LevelPlayAdInfo` and pass them to `CloudXArbiterBid.levelPlay()`. The CloudX bid takes the `CloudXAd` directly. `listOfNotNull` submits only the platforms that actually loaded. | `LevelPlayAdInfo` field | Type | `CloudXArbiterBid.levelPlay` parameter | | ----------------------- | -------- | -------------------------------------- | | `adNetwork` | `String` | `networkName` | | `revenue` | `Double` | `revenue` | | `precision` | `String` | `precision` | ```kotlin val bids = listOfNotNull( cloudXAd?.let { CloudXArbiterBid.cloudX(it) }, levelPlayInfo?.let { info -> CloudXArbiterBid.levelPlay( networkName = info.adNetwork, // LevelPlayAdInfo.adNetwork revenue = info.revenue, // LevelPlayAdInfo.revenue precision = info.precision, // LevelPlayAdInfo.precision ) }, ) ``` 4. **Run the arbiter and store the winner.** Once both candidates have settled, wrap the bids in a `CloudXArbiterConfiguration` and pass it to `CloudX.arbiter()` with a listener. Store the result — do not show anything yet. `onCompleted()` runs on the main thread. ```kotlin private var nextWinner: CloudXArbiterResult? = null private fun prepareWinner() { val configuration = CloudXArbiterConfiguration.builder(bids).build() CloudX.arbiter(configuration, object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { nextWinner = result } }) } ``` 5. **Show the stored winner at the placement.** When the placement is reached, switch on the stored `platform` and show that platform's ad. There is no arbiter call on this path. A stored `CloudXArbiterPlatform.NONE`, or no stored result at all, means there is no winner to show — continue without showing an ad. ```kotlin fun showAtPlacement(activity: Activity) { when (nextWinner?.platform) { CloudXArbiterPlatform.CLOUDX -> cloudXInterstitial.show(activity) CloudXArbiterPlatform.LEVELPLAY -> levelPlayInterstitial.showAd(activity) else -> { } // no winner prepared; continue without an ad } nextWinner = null } ``` The `ArbiterInterstitialController` below packages these same steps into a reusable component. ## Interstitial example This controller is the reference implementation of the prepare-ahead rule for fullscreen formats. It arbitrates between two platforms — CloudX and Unity LevelPlay — and prepares 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. ```kotlin ArbiterInterstitialController.kt /** * Prepares a Trusted Arbiter winner ahead of time so an interstitial can be shown * instantly when a placement is reached. * * Loads [cloudXInterstitial] and [levelPlayInterstitial] in parallel, waits until both * have finished loading or failing, submits the loaded candidates to [CloudX.arbiter], * and caches the selected [CloudXArbiterPlatform] in [nextWinner]. */ class ArbiterInterstitialController( private val cloudXInterstitial: CloudXInterstitialAd, private val levelPlayInterstitial: LevelPlayInterstitialAd, ) { /** Receives the arbitration outcome once a winner has been prepared. */ interface Listener { /** Called when the arbiter has selected [platform] for the next show. */ fun onWinnerPrepared(platform: CloudXArbiterPlatform) } /** Set to observe [Listener.onWinnerPrepared] callbacks. */ var listener: Listener? = null private var cloudXAd: CloudXAd? = null private var cloudXLoadDone = false private var levelPlayAdInfo: LevelPlayAdInfo? = null private var levelPlayLoadDone = false private var nextWinner: CloudXArbiterPlatform? = null init { cloudXInterstitial.listener = createCloudXListener() levelPlayInterstitial.setListener(createLevelPlayListener()) } /** Starts a load for each platform that does not currently hold a cached ad. */ fun loadMissingAds() { if (cloudXAd == null) cloudXInterstitial.load() if (levelPlayAdInfo == null) levelPlayInterstitial.loadAd() } /** * Shows the prepared winner on [activity], 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. */ fun showAtPlacement(activity: Activity): Boolean { return when (nextWinner) { CloudXArbiterPlatform.CLOUDX -> showCloudX(activity) CloudXArbiterPlatform.LEVELPLAY -> showLevelPlay(activity) else -> 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 [CloudX.arbiter]. */ private fun maybePrepareWinner() { if (!cloudXLoadDone || !levelPlayLoadDone) return if (cloudXAd == null && levelPlayAdInfo == null) { cloudXLoadDone = false levelPlayLoadDone = false loadMissingAds() return } val bids = listOfNotNull( cloudXAd?.let { CloudXArbiterBid.cloudX(it) }, levelPlayAdInfo?.let { adInfo -> CloudXArbiterBid.levelPlay( networkName = adInfo.adNetwork, revenue = adInfo.revenue, precision = adInfo.precision, ) } ) CloudX.arbiter( configuration = CloudXArbiterConfiguration.builder(bids).build(), listener = object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { nextWinner = result.platform listener?.onWinnerPrepared(result.platform) } } ) } private fun showCloudX(activity: Activity): Boolean { if (cloudXInterstitial.isAdReady) { cloudXInterstitial.show(activity) return true } clearCloudXAndLoadMissingAds() return false } private fun showLevelPlay(activity: Activity): Boolean { if (levelPlayInterstitial.isAdReady) { levelPlayInterstitial.showAd(activity) return true } clearLevelPlayAndLoadMissingAds() return false } private fun clearCloudXAndLoadMissingAds() { cloudXAd = null cloudXLoadDone = false nextWinner = null loadMissingAds() } private fun clearLevelPlayAndLoadMissingAds() { levelPlayAdInfo = null levelPlayLoadDone = false nextWinner = null loadMissingAds() } private fun createCloudXListener() = object : CloudXInterstitialListener { override fun onAdLoaded(cloudXAd: CloudXAd) { this@ArbiterInterstitialController.cloudXAd = cloudXAd cloudXLoadDone = true maybePrepareWinner() } override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) { cloudXAd = null cloudXLoadDone = true maybePrepareWinner() } override fun onAdDisplayed(cloudXAd: CloudXAd) = Unit override fun onAdDisplayFailed(cloudXAd: CloudXAd, cloudXError: CloudXError) { clearCloudXAndLoadMissingAds() } override fun onAdHidden(cloudXAd: CloudXAd) { clearCloudXAndLoadMissingAds() } override fun onAdClicked(cloudXAd: CloudXAd) = Unit } private fun createLevelPlayListener() = object : LevelPlayInterstitialAdListener { override fun onAdLoaded(levelPlayAdInfo: LevelPlayAdInfo) { this@ArbiterInterstitialController.levelPlayAdInfo = levelPlayAdInfo levelPlayLoadDone = true maybePrepareWinner() } override fun onAdLoadFailed(levelPlayAdError: LevelPlayAdError) { levelPlayAdInfo = null levelPlayLoadDone = true maybePrepareWinner() } override fun onAdInfoChanged(levelPlayAdInfo: LevelPlayAdInfo) { this@ArbiterInterstitialController.levelPlayAdInfo = levelPlayAdInfo } override fun onAdDisplayed(levelPlayAdInfo: LevelPlayAdInfo) = Unit override fun onAdDisplayFailed( levelPlayAdError: LevelPlayAdError, levelPlayAdInfo: LevelPlayAdInfo ) { clearLevelPlayAndLoadMissingAds() } override fun onAdClosed(levelPlayAdInfo: LevelPlayAdInfo) { clearLevelPlayAndLoadMissingAds() } override fun onAdClicked(levelPlayAdInfo: LevelPlayAdInfo) = Unit } } ``` `showAtPlacement()` returns `true` only when an ad show call was made. `onAdInfoChanged()` keeps the cached LevelPlay candidate up to date while it remains loaded. For PubMatic OpenWrap, create a third-party bid with `CloudXArbiterBid.pubmatic(price, partnerName)`. If the arbiter service is unavailable, the SDK falls back to the highest comparable USD bid among the supplied supported bid inputs. ## App Open example App Open is a fullscreen format, so it prepares a winner ahead of the placement exactly like interstitial and rewarded. What differs is the trigger. Nothing in your UI asks for an App Open ad, so the decision has to be made while the app is in the background — a user returning to the app must never wait on a load or an arbiter round trip. `ArbiterAppOpenController` arbitrates a CloudX App Open ad against an AdMob App Open ad: 1. Load both platforms at startup, and again after every show. 2. Run the arbiter once both have settled, and store the winner. 3. On the next foreground transition, show the stored winner and start the next cycle. 4. If no winner is stored, return the user to the app without an ad and start a fresh load. ```kotlin ArbiterAppOpenController.kt /** * Arbitrates a CloudX App Open ad against an AdMob App Open ad and shows the winner on the * next foreground transition. * * App Open has no user-initiated show, so the winner must already be chosen by the time the * user comes back. [loadMissingAds] starts both loads, the arbiter runs as soon as both * platforms settle, and [showOnForeground] only shows a result that was stored earlier. */ class ArbiterAppOpenController( private val context: Context, cloudXAdUnitId: String, private val adMobAdUnitId: String, ) { /** Receives the arbitration outcome once a winner has been prepared. */ interface Listener { /** Called when the arbiter has selected [platform] for the next foreground show. */ fun onWinnerPrepared(platform: CloudXArbiterPlatform) } /** Set to observe [Listener.onWinnerPrepared] callbacks. */ var listener: Listener? = null private val cloudXAppOpen: CloudXAppOpenAd = CloudX.createAppOpen(context, cloudXAdUnitId) private var cloudXAd: CloudXAd? = null private var cloudXLoadDone = false private var adMobAppOpen: AppOpenAd? = null /** The ad that is presenting, or presented last. Held so a late paid event still has an ad. */ private var presentedAdMobAd: AppOpenAd? = null private var adMobLoadDone = false private var cloudXLoadInFlight = false private var adMobLoadInFlight = false private var nextWinner: CloudXArbiterPlatform? = null private var isShowingAd = false init { cloudXAppOpen.listener = createCloudXListener() } /** * Starts a load for each platform that holds neither a cached ad nor a request in flight. * * Each load it starts clears that platform's `LoadDone` flag, so a round only arbitrates once * every request it started has come back. Without that reset, a retry for the platform that * failed last round lands against a flag still set from the previous round, and the arbiter * runs on a one-candidate snapshot. */ fun loadMissingAds() { if (cloudXAd == null && !cloudXLoadInFlight) { cloudXLoadInFlight = true cloudXLoadDone = false cloudXAppOpen.load() } if (adMobAppOpen == null && !adMobLoadInFlight) { adMobLoadInFlight = true adMobLoadDone = false loadAdMob() } } /** * Shows the prepared winner on [activity], returning true only when a show call was made. * * Call this from a foreground observer. It never holds up the return to the app: with an ad * already on screen, or no winner prepared, it returns false and starts the next load cycle. */ fun showOnForeground(activity: Activity): Boolean { if (isShowingAd) return false return when (nextWinner) { CloudXArbiterPlatform.CLOUDX -> showCloudX(activity) CloudXArbiterPlatform.ADMOB -> showAdMob(activity) CloudXArbiterPlatform.NONE -> { // The arbiter settled on no winner, so both cached ads are unusable. Drop them: // loadMissingAds() only requests a platform whose slot is empty, so without this // the same two candidates would sit there and no later round could ever run. discardCandidatesAndReload() false } // nextWinner is null — a cycle is still in flight. Leave it alone. else -> false } } /** Releases both platforms' ads. Call this from the owning component's teardown. */ fun destroy() { cloudXAppOpen.destroy() adMobAppOpen = null presentedAdMobAd = null } /** * 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 [CloudX.arbiter]. */ private fun maybePrepareWinner() { if (!cloudXLoadDone || !adMobLoadDone) return if (cloudXAd == null && adMobAppOpen == null) { loadMissingAds() return } val bids = listOfNotNull( cloudXAd?.let { CloudXArbiterBid.cloudX(it) }, adMobAppOpen?.let { CloudXArbiterBid.adMob(adUnitId = adMobAdUnitId) }, ) CloudX.arbiter( configuration = CloudXArbiterConfiguration.builder(bids).build(), listener = object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { nextWinner = result.platform listener?.onWinnerPrepared(result.platform) } } ) } private fun showCloudX(activity: Activity): Boolean { if (cloudXAppOpen.isAdReady) { isShowingAd = true cloudXAppOpen.show(activity, "app_foreground") return true } clearCloudXAndLoadMissingAds() return false } private fun showAdMob(activity: Activity): Boolean { val ad = adMobAppOpen if (ad == null) { clearAdMobAndLoadMissingAds() return false } ad.fullScreenContentCallback = object : FullScreenContentCallback() { override fun onAdDismissedFullScreenContent() { clearAdMobAndLoadMissingAds() } override fun onAdFailedToShowFullScreenContent(adError: AdError) { Log.e("CloudX", "AdMob App Open failed to show: ${adError.message}") clearAdMobAndLoadMissingAds() } } // An AppOpenAd is single-use: show() consumes it. Move it aside now, so a load that // lands mid-show cannot arbitrate a spent ad. It stays alive in presentedAdMobAd // because AdMob can deliver the paid event after the ad has already been dismissed, // and that revenue is what prices the next AdMob bid. presentedAdMobAd = ad adMobAppOpen = null isShowingAd = true ad.show(activity) return true } private fun loadAdMob() { AppOpenAd.load( context, adMobAdUnitId, AdRequest.Builder().build(), object : AppOpenAd.AppOpenAdLoadCallback() { override fun onAdLoaded(ad: AppOpenAd) { ad.setOnPaidEventListener { adValue -> reportAdMobPaidEvent(ad, adValue) } adMobAppOpen = ad adMobLoadInFlight = false adMobLoadDone = true maybePrepareWinner() } override fun onAdFailedToLoad(error: LoadAdError) { Log.w("CloudX", "AdMob App Open failed to load: ${error.message}") adMobAppOpen = null adMobLoadInFlight = false adMobLoadDone = true maybePrepareWinner() } }, ) } /** * Forwards AdMob's realized revenue to CloudX. This is a required part of the integration, * not optional analytics: CloudX prices future AdMob bids from what you report back. * * toCloudXRevenuePrecision() is the mapping shown in * "Report Google paid events back to CloudX" above. */ private fun reportAdMobPaidEvent(ad: AppOpenAd, adValue: AdValue) { val servedBy = ad.responseInfo.loadedAdapterResponseInfo val accepted = CloudX.reportRevenueData( CloudXRevenueData.builder( platform = CloudXRevenuePlatform.ADMOB, revenue = adValue.valueMicros / 1_000_000.0, adFormat = "app_open", ) .currencyCode(adValue.currencyCode) .precision(adValue.precisionType.toCloudXRevenuePrecision()) .networkName(servedBy?.adSourceName) .adUnitId(adMobAdUnitId) .thirdPartyAdPlacementId(servedBy?.adSourceInstanceName) .build(), ) if (!accepted) { Log.w("CloudX", "AdMob App Open paid event was not accepted by the CloudX SDK") } } /** Drops both candidates and starts a new cycle. Used when the arbiter reports no winner. */ private fun discardCandidatesAndReload() { cloudXAd = null adMobAppOpen = null cloudXLoadDone = false adMobLoadDone = false nextWinner = null loadMissingAds() } private fun clearCloudXAndLoadMissingAds() { cloudXAd = null cloudXLoadDone = false nextWinner = null isShowingAd = false loadMissingAds() } private fun clearAdMobAndLoadMissingAds() { adMobAppOpen = null adMobLoadDone = false nextWinner = null isShowingAd = false loadMissingAds() } private fun createCloudXListener() = object : CloudXAppOpenListener { override fun onAdLoaded(cloudXAd: CloudXAd) { this@ArbiterAppOpenController.cloudXAd = cloudXAd cloudXLoadInFlight = false cloudXLoadDone = true maybePrepareWinner() } override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) { Log.w("CloudX", "CloudX App Open failed to load: ${cloudXError.message}") cloudXAd = null cloudXLoadInFlight = false cloudXLoadDone = true maybePrepareWinner() } override fun onAdDisplayed(cloudXAd: CloudXAd) = Unit override fun onAdDisplayFailed(cloudXAd: CloudXAd, cloudXError: CloudXError) { Log.e("CloudX", "CloudX App Open failed to display: ${cloudXError.message}") clearCloudXAndLoadMissingAds() } override fun onAdHidden(cloudXAd: CloudXAd) { clearCloudXAndLoadMissingAds() } override fun onAdClicked(cloudXAd: CloudXAd) = Unit } } ``` `showOnForeground()` returns `true` only when a show call was made. `isShowingAd` keeps the ad's own fullscreen presentation from being read as a second foreground transition — without it, a lifecycle observer can re-enter `showOnForeground()` while an ad is already on screen. Drive it from the process lifecycle, so the show happens on a real return to the foreground rather than on every activity resume: ```kotlin // In the class that owns the controller, for example your Application subclass: private val controller = ArbiterAppOpenController(context, cloudXAdUnitId, adMobAdUnitId) ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { currentActivity?.let { controller.showOnForeground(it) } } }) ``` Both platforms report a winning App Open bid the same way as any other format: `CloudXArbiterPlatform.CLOUDX` or `CloudXArbiterPlatform.ADMOB`. Keep the App Open placement rules from [App Open Ads](/en/android/ad-formats/app-open) — arbitration changes which network fills the placement, not when it is appropriate to show one. ## Banner and MREC arbitration Banner and MREC are view formats: multiple networks can hold a loaded ad at the same time, but only one view can be on screen. Arbitrating between them adds three requirements that fullscreen formats don't have. > **Warning:** Only attach the winning bid's view to the view hierarchy. Attaching a losing view renders it and fires an impression for a bid that Trusted Arbiter did not select. ### Disable auto-refresh Auto-refresh must be turned off everywhere so the arbiter — not each network's internal timer — controls when a new ad is shown: * Turn off auto-refresh for the ad unit in the CloudX dashboard. * Call `stopAutoRefresh()` on the `CloudXAdView` immediately after creating it. * Disable auto-refresh on the other arbitrated networks' banner APIs as well. ### View attachment Load every candidate off-screen. Only the winning bid's banner view may be attached to the view hierarchy or displayed. Keep losing views unattached — do not add them to a parent view, even temporarily — since attaching a view renders it and can fire an impression for a bid the arbiter rejected. Unlike the standard banner integration, which adds the view to the layout at creation time (see [Banner & MREC](/en/android/ad-formats/banner-mrec)), with Trusted Arbiter the view must not be attached at creation — attach it only after arbitration selects it as the winner. ### Refresh cycle With auto-refresh disabled, the arbiter must drive its own refresh loop: 1. **Run the first round.** Load candidates from every network in parallel, run the arbiter, and attach the winning view. 2. **Start the next load on the winner.** As soon as the winner's impression fires, immediately start loading a new fill from the winning network so it's ready for the next round. 3. **Retain non-winning fills.** Keep the non-winning networks' already-filled ads for the next arbitration round. Re-request loads only from networks that did not fill in the previous round. 4. **Re-arbitrate.** Once the outstanding load responses return, run the arbiter again over the full candidate set. 5. **Swap on an interval.** Refresh the displayed ad on a 20–30 second interval by attaching the new winner's view in place of the old one. Intervals shorter than 20 seconds decrease CPM performance. ### Banner example This controller is the reference implementation of the arbitrate-then-render rule for view formats. It arbitrates a CloudX banner against a LevelPlay banner and drives the refresh cycle above. The same structure applies to MREC — swap `CloudX.createBanner()` for `CloudX.createMREC()` and the LevelPlay banner type for the LevelPlay MREC type. ```kotlin ArbiterBannerController.kt /** * Drives Trusted Arbiter for a banner placement: loads [cloudXBanner] and * [levelPlayBanner] in parallel, arbitrates between whichever candidates filled, * attaches the winning view to [container], and refreshes on [refreshIntervalMs]. * * Auto-refresh must be disabled on both networks (dashboard setting for CloudX, * `stopAutoRefresh()` here, and the equivalent LevelPlay setting) — this controller * owns the refresh cycle instead. */ class ArbiterBannerController( private val cloudXBanner: CloudXAdView, private val levelPlayBanner: LevelPlayBannerAdView, private val container: ViewGroup, private val refreshIntervalMs: Long = 25_000L, ) { private val handler = Handler(Looper.getMainLooper()) private var cloudXAd: CloudXAd? = null private var cloudXFilled = false private var cloudXLoadDone = false private var levelPlayAdInfo: LevelPlayAdInfo? = null private var levelPlayFilled = false private var levelPlayLoadDone = false private var currentWinner: CloudXArbiterPlatform? = null private val refreshRunnable = Runnable { runArbitrationRound() } init { cloudXBanner.stopAutoRefresh() cloudXBanner.listener = createCloudXListener() cloudXBanner.revenueListener = createCloudXRevenueListener() levelPlayBanner.bannerListener = createLevelPlayListener() } /** Starts the first round: loads every network that isn't already holding a fill. */ fun start() { loadMissingAds() } fun stop() { handler.removeCallbacks(refreshRunnable) } private fun loadMissingAds() { if (!cloudXFilled) cloudXBanner.load() if (!levelPlayFilled) levelPlayBanner.loadAd() } /** Runs once both networks have settled (filled or failed); re-arbitrates only pending candidates. */ private fun maybeArbitrate() { if (!cloudXLoadDone || !levelPlayLoadDone) return val bids = listOfNotNull( cloudXAd?.let { CloudXArbiterBid.cloudX(it) }, levelPlayAdInfo?.let { info -> CloudXArbiterBid.levelPlay( networkName = info.adNetwork, revenue = info.revenue, precision = info.precision, ) }, ) if (bids.isEmpty()) { loadMissingAds() return } CloudX.arbiter( configuration = CloudXArbiterConfiguration.builder(bids).build(), listener = object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { showWinner(result.platform) } } ) } private fun runArbitrationRound() { maybeArbitrate() } /** Attaches only the winning view; the losing view is never added to [container]. */ private fun showWinner(platform: CloudXArbiterPlatform) { currentWinner = platform container.removeAllViews() when (platform) { CloudXArbiterPlatform.CLOUDX -> container.addView(cloudXBanner) CloudXArbiterPlatform.LEVELPLAY -> container.addView(levelPlayBanner) else -> return // CloudXArbiterPlatform.NONE — no winner; leave container empty } handler.removeCallbacks(refreshRunnable) handler.postDelayed(refreshRunnable, refreshIntervalMs) } /** Starts the winning network's next load immediately after its impression fires. */ private fun onWinnerImpression(platform: CloudXArbiterPlatform) { when (platform) { CloudXArbiterPlatform.CLOUDX -> { cloudXFilled = false cloudXLoadDone = false cloudXBanner.load() } CloudXArbiterPlatform.LEVELPLAY -> { levelPlayFilled = false levelPlayLoadDone = false levelPlayBanner.loadAd() } else -> Unit } } private fun createCloudXListener() = object : CloudXAdViewListener { override fun onAdLoaded(cloudXAd: CloudXAd) { this@ArbiterBannerController.cloudXAd = cloudXAd cloudXFilled = true cloudXLoadDone = true maybeArbitrate() } override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) { cloudXAd = null cloudXFilled = false cloudXLoadDone = true maybeArbitrate() } override fun onAdClicked(cloudXAd: CloudXAd) = Unit override fun onAdExpanded(cloudXAd: CloudXAd) = Unit override fun onAdCollapsed(cloudXAd: CloudXAd) = Unit } private fun createCloudXRevenueListener() = object : CloudXAdRevenueListener { override fun onAdRevenuePaid(cloudXAd: CloudXAd) { // onAdRevenuePaid fires at CloudX impression time; only act on it while CloudX is showing. if (currentWinner == CloudXArbiterPlatform.CLOUDX) { onWinnerImpression(CloudXArbiterPlatform.CLOUDX) } } } private fun createLevelPlayListener() = object : LevelPlayBannerAdViewListener { override fun onAdLoaded(levelPlayAdInfo: LevelPlayAdInfo) { this@ArbiterBannerController.levelPlayAdInfo = levelPlayAdInfo levelPlayFilled = true levelPlayLoadDone = true maybeArbitrate() } override fun onAdLoadFailed(levelPlayAdError: LevelPlayAdError) { levelPlayAdInfo = null levelPlayFilled = false levelPlayLoadDone = true maybeArbitrate() } override fun onAdDisplayed(levelPlayAdInfo: LevelPlayAdInfo) { if (currentWinner == CloudXArbiterPlatform.LEVELPLAY) { onWinnerImpression(CloudXArbiterPlatform.LEVELPLAY) } } override fun onAdClicked(levelPlayAdInfo: LevelPlayAdInfo) = Unit } } ``` `onAdRevenuePaid()` (CloudX) and `onAdDisplayed()` (LevelPlay) are where each network's impression fires; that is where the winning network's next load is kicked off, keeping a fresh fill ready for the following round without holding up the current display. Networks that already have a filled, unattached ad skip straight past `loadMissingAds()` until their fill is used or expires. ## Native example Native is a view format, so it arbitrates and then renders — but unlike banner and MREC, a native ad has no view until you make one. That is what makes native the easiest format to get wrong: `loadAd(adView)` renders the ad at load time, before the arbiter has chosen anything. > **Warning:** Load native candidates with `loadAd()` and no ad view. Rendering is what fires a native impression, so rendering a bid the arbiter has not selected bills an impression against a loser. Call `render()` only for the winner. `ArbiterNativeController` arbitrates a CloudX native ad against an AdMob native ad and renders exactly one of them into the container: 1. Load both platforms with no view attached. 2. Run the arbiter once both have settled. 3. Build a view for the winner only, render into it, and attach it. 4. Keep the losing platform's ad loaded and unrendered for the next round. ```kotlin ArbiterNativeController.kt /** * Arbitrates a CloudX native ad against an AdMob native ad and renders only the winner * into [container]. * * Both platforms load without a view: [CloudXNativeAdLoader.loadAd] is called with no ad view * so nothing is rendered at load time, and the AdMob ad is held unrendered. The losing bid's * assets are never rendered or attached, so they never fire an impression. */ class ArbiterNativeController( private val context: Context, private val container: ViewGroup, private val binder: CloudXNativeAdViewBinder, cloudXAdUnitId: String, private val adMobAdUnitId: String, ) { private val cloudXLoader: CloudXNativeAdLoader = CloudX.createNativeAdLoader(context, cloudXAdUnitId) private var cloudXAd: CloudXAd? = null private var cloudXLoadDone = false private var adMobNativeAd: NativeAd? = null private var adMobLoadDone = false private var cloudXLoadInFlight = false private var adMobLoadInFlight = false private var renderedPlatform: CloudXArbiterPlatform? = null private var renderedAdView: View? = null // The ads behind the attached view. They outlive their own candidate state: a rendered ad // stays on screen after its impression, until its replacement is attached. private var renderedCloudXAd: CloudXAd? = null private var renderedAdMobAd: NativeAd? = null init { cloudXLoader.nativeAdListener = createCloudXListener() cloudXLoader.revenueListener = createCloudXRevenueListener() } /** Starts the first round: loads every platform that is not already holding a fill. */ fun start() { loadMissingAds() } /** Releases both platforms' ads and detaches the rendered view. */ fun destroy() { // detachRenderedAd() releases the rendered AdMob ad; only destroy the candidate here // when it is a different, never-rendered instance. val unrenderedAdMobAd = adMobNativeAd.takeIf { it !== renderedAdMobAd } detachRenderedAd() cloudXLoader.destroy() unrenderedAdMobAd?.destroy() adMobNativeAd = null } /** * Starts a load for each platform that holds neither a filled ad nor a request in flight. * * Each load it starts clears that platform's `LoadDone` flag, so a round only arbitrates once * every request it started has come back. */ private fun loadMissingAds() { // loadAd() with no ad view defers rendering. Never use loadAd(adView) here: that // renders the CloudX ad at load time, before the arbiter has chosen a winner. if (cloudXAd == null && !cloudXLoadInFlight) { cloudXLoadInFlight = true cloudXLoadDone = false cloudXLoader.loadAd() } if (adMobNativeAd == null && !adMobLoadInFlight) { adMobLoadInFlight = true adMobLoadDone = false loadAdMob() } } /** Runs once both platforms have settled (filled or failed), then renders the winner. */ private fun maybeArbitrate() { if (!cloudXLoadDone || !adMobLoadDone) return val bids = listOfNotNull( cloudXAd?.let { CloudXArbiterBid.cloudX(it) }, adMobNativeAd?.let { CloudXArbiterBid.adMob(adUnitId = adMobAdUnitId) }, ) if (bids.isEmpty()) { loadMissingAds() return } CloudX.arbiter( configuration = CloudXArbiterConfiguration.builder(bids).build(), listener = object : CloudXArbiterListener { override fun onCompleted(result: CloudXArbiterResult) { renderWinner(result.platform) } } ) } /** Renders only the winning platform's assets; the loser stays unrendered and unattached. */ private fun renderWinner(platform: CloudXArbiterPlatform) { // Rendering is what fires the impression, so never render the same ad twice. // maybeArbitrate() runs again whenever the losing candidate reloads, and the same // platform winning that round is a normal outcome — but by then the served ad has been // dropped as a candidate, so a repeat win is always a new ad and does render. if (isAlreadyRendered(platform)) return if (platform == CloudXArbiterPlatform.NONE) { // No winner. Keep whatever is already on screen — detaching it here would drop a // perfectly good ad — and drop only the candidates that lost, so the next round has // something new to compare. discardUnrenderedCandidatesAndReload() return } detachRenderedAd() when (platform) { CloudXArbiterPlatform.CLOUDX -> renderCloudX() CloudXArbiterPlatform.ADMOB -> renderAdMob() else -> Unit } } /** True when this platform's current candidate is the ad already attached to the container. */ private fun isAlreadyRendered(platform: CloudXArbiterPlatform): Boolean = when (platform) { CloudXArbiterPlatform.CLOUDX -> cloudXAd != null && cloudXAd === renderedCloudXAd CloudXArbiterPlatform.ADMOB -> adMobNativeAd != null && adMobNativeAd === renderedAdMobAd else -> false } /** Drops every candidate that is not currently rendered, then starts a new cycle. */ private fun discardUnrenderedCandidatesAndReload() { if (renderedPlatform != CloudXArbiterPlatform.CLOUDX) { cloudXAd = null cloudXLoadDone = false } if (renderedPlatform != CloudXArbiterPlatform.ADMOB) { adMobNativeAd = null adMobLoadDone = false } loadMissingAds() } /** * Removes only the ad view this controller attached, so anything else the publisher keeps in * the container — a placeholder, a spinner, a label — survives the round. * * This is also the only place a rendered ad is released. It stays alive for as long as it is * on screen, and goes away when its replacement is attached or the controller is destroyed. */ private fun detachRenderedAd() { renderedAdView?.let { container.removeView(it) } renderedAdView = null renderedPlatform = null renderedCloudXAd?.let { cloudXLoader.destroy(it) } renderedCloudXAd = null renderedAdMobAd?.destroy() renderedAdMobAd = null } private fun renderCloudX() { val ad = cloudXAd ?: return val adView = CloudXNativeAdView(context, binder) cloudXLoader.render(adView, ad) container.addView(adView) renderedAdView = adView renderedPlatform = CloudXArbiterPlatform.CLOUDX renderedCloudXAd = ad } private fun renderAdMob() { val ad = adMobNativeAd ?: return // Inflate your own NativeAdView layout and assign its asset views, exactly as you // would in a non-arbitrated AdMob native integration. val adView = LayoutInflater.from(context) .inflate(R.layout.admob_native_ad_layout, container, false) as NativeAdView adView.setNativeAd(ad) container.addView(adView) renderedAdView = adView renderedPlatform = CloudXArbiterPlatform.ADMOB renderedAdMobAd = ad } private fun loadAdMob() { AdLoader.Builder(context, adMobAdUnitId) .forNativeAd { nativeAd -> nativeAd.setOnPaidEventListener { adValue -> reportAdMobPaidEvent(nativeAd, adValue) } adMobNativeAd = nativeAd adMobLoadInFlight = false adMobLoadDone = true maybeArbitrate() } .withAdListener(object : AdListener() { override fun onAdFailedToLoad(error: LoadAdError) { Log.w("CloudX", "AdMob native ad failed to load: ${error.message}") adMobNativeAd = null adMobLoadInFlight = false adMobLoadDone = true maybeArbitrate() } // The winning AdMob ad is consumed the moment its impression fires, so request // the next one here. Without this the controller keeps bidding a spent ad for // the rest of the session. override fun onAdImpression() { if (renderedPlatform == CloudXArbiterPlatform.ADMOB) { onWinnerImpression(CloudXArbiterPlatform.ADMOB) } } }) .build() .loadAd(AdRequest.Builder().build()) } /** * Forwards AdMob's realized revenue to CloudX. This is a required part of the integration, * not optional analytics: CloudX prices future AdMob bids from what you report back. * * toCloudXRevenuePrecision() is the mapping shown in * "Report Google paid events back to CloudX" above. */ private fun reportAdMobPaidEvent(ad: NativeAd, adValue: AdValue) { val servedBy = ad.responseInfo?.loadedAdapterResponseInfo val accepted = CloudX.reportRevenueData( CloudXRevenueData.builder( platform = CloudXRevenuePlatform.ADMOB, revenue = adValue.valueMicros / 1_000_000.0, adFormat = "native", ) .currencyCode(adValue.currencyCode) .precision(adValue.precisionType.toCloudXRevenuePrecision()) .networkName(servedBy?.adSourceName) .adUnitId(adMobAdUnitId) .thirdPartyAdPlacementId(servedBy?.adSourceInstanceName) .build(), ) if (!accepted) { Log.w("CloudX", "AdMob native paid event was not accepted by the CloudX SDK") } } private fun createCloudXListener() = object : CloudXNativeAdListener { // adView is null here because loadAd() was called without one. The ad is rendered // later, and only if it wins. override fun onNativeAdLoaded(adView: CloudXNativeAdView?, ad: CloudXAd) { cloudXAd = ad cloudXLoadInFlight = false cloudXLoadDone = true maybeArbitrate() } override fun onNativeAdLoadFailed(adUnitId: String, error: CloudXError) { Log.w("CloudX", "CloudX native ad failed to load: ${error.message}") cloudXAd = null cloudXLoadInFlight = false cloudXLoadDone = true maybeArbitrate() } override fun onNativeAdClicked(ad: CloudXAd) = Unit override fun onNativeAdExpired(ad: CloudXAd) { // detachRenderedAd() releases the rendered ad, so hand it this one instead of // destroying it twice. if (renderedCloudXAd !== ad) cloudXLoader.destroy(ad) clearCloudXAndReload() } override fun onNativeAdClosed(ad: CloudXAd) { if (renderedCloudXAd !== ad) cloudXLoader.destroy(ad) clearCloudXAndReload() } } private fun createCloudXRevenueListener() = object : CloudXAdRevenueListener { override fun onAdRevenuePaid(cloudXAd: CloudXAd) { // onAdRevenuePaid fires at CloudX impression time; only act on it while CloudX is // the platform on screen. if (renderedPlatform == CloudXArbiterPlatform.CLOUDX) { onWinnerImpression(CloudXArbiterPlatform.CLOUDX) } } } /** * Starts the next cycle once the winner's impression has fired. * * The rendered view stays on screen and the ad behind it stays alive. A native slot has * nothing to fall back to, so detaching the ad the moment it is billed would leave the slot * blank for as long as the next round takes. Only the candidate state is dropped, so the * spent ad is never bid or rendered again; the ad itself is released in detachRenderedAd(), * when its replacement goes up. This is the same rule the banner controller follows. */ private fun onWinnerImpression(platform: CloudXArbiterPlatform) { when (platform) { CloudXArbiterPlatform.CLOUDX -> { cloudXAd = null cloudXLoadDone = false cloudXLoadInFlight = false } CloudXArbiterPlatform.ADMOB -> { adMobNativeAd = null adMobLoadDone = false adMobLoadInFlight = false } else -> return } loadMissingAds() } /** Drops the CloudX candidate, detaching it first if it is the one on screen. */ private fun clearCloudXAndReload() { if (renderedPlatform == CloudXArbiterPlatform.CLOUDX) { detachRenderedAd() } cloudXAd = null cloudXLoadDone = false cloudXLoadInFlight = false loadMissingAds() } } ``` `onNativeAdLoaded()` delivers a null `adView` because the load was started without one — that null is the signal that rendering is still yours to trigger. Build the `CloudXNativeAdView` in `renderCloudX()` and pass it to `render()` only after the arbiter has named CloudX the winner. The losing platform keeps its filled ad for the next round, so only the platform that was consumed or expired is re-requested. A rendered ad is spent, so each platform drops it as a candidate as soon as the impression fires — `onAdRevenuePaid` for CloudX, `onAdImpression()` for AdMob — and requests a new one. Neither handler detaches the view. A native slot has nothing to fall back to, so removing the served ad at its impression would leave the slot blank for as long as the next round takes. The rendered ad stays up and is released in `detachRenderedAd()`, at the moment its replacement is attached or the controller is destroyed — the same rule the banner controller follows. An ad that expires or is closed while it is on screen is the one case that does detach early, which is why `renderedPlatform` is tracked. `renderWinner()` returns early when the arbiter picks the platform already on screen, because rendering is what fires the impression and that round did not change the winner. To refresh a native slot on a timer, use the same cycle as [Banner and MREC arbitration](#banner-and-mrec-arbitration): re-arbitrate on a 20–30 second interval and swap the rendered view for the new winner's. For a Reels-style feed, create one controller per slot — see [Native Ads](/en/android/ad-formats/native). ## Custom Bid Inputs Use `CloudXArbiterBid.custom()` when you want Trusted Arbiter to compare CloudX with a third-party platform that does not have a dedicated bid helper. ```kotlin val customBid = CloudXArbiterBid.custom( platformName = "my_mediation_platform", networkName = "winning_demand_source", revenuePerImpressionUSD = 0.00125, precision = CloudXArbiterPrecision.EXACT, extras = mapOf("ad_unit" to "third-party-ad-unit-id"), ) val configuration = CloudXArbiterConfiguration.builder( listOf(CloudXArbiterBid.cloudX(cloudXAd), customBid) ).build() ``` When a custom bid wins, `result.platform` is `CloudXArbiterPlatform.CUSTOM` and `result.platformName` contains the `platformName` supplied on the bid. Pass `revenuePerImpressionUSD` as revenue for one impression in USD, not CPM. Use `CloudXArbiterPrecision.EXACT`, `ESTIMATED`, `PUBLISHER_DEFINED`, or `UNDEFINED` to describe that revenue value. --- # Banner & MREC Ads > Integrate CloudX banner and MREC ads in Android apps #### CloudX-rendered creatives CloudX-served creatives are rendered by the built-in renderer, which ships inside the core SDK — no separate renderer adapter module is required. The creative type is selected automatically from the bid response, so no publisher integration is needed to render either format. The renderer supports MRAID 3.0 (HTML) and VAST creatives across the ad formats: | Ad format | HTML (MRAID 3.0) | VAST | | ------------ | ---------------- | ---- | | Banner | ✓ | — | | MREC | ✓ | ✓ | | Interstitial | ✓ | ✓ | | Rewarded | ✓ | ✓ | With SDK 4.7.0 or later, CloudX-rendered MREC VAST video starts muted and pauses off-screen. Auto-refresh pauses during MREC VAST playback and resumes after playback ends. Standard 320x50 banners do not support VAST creatives. HTML/MRAID video in CloudX-rendered banner and MREC ads also starts muted. Unmuting requires a user gesture on that video’s controls; a tap elsewhere in the ad does not enable audio. ## Banner Ads (320x50) ```kotlin Kotlin class MainActivity : AppCompatActivity(), CloudXAdViewListener, CloudXAdRevenueListener { private lateinit var bannerAd: CloudXAdView private fun createBannerAd() { bannerAd = CloudX.createBanner(this, "your-banner-ad-unit-id") bannerAd.listener = this bannerAd.revenueListener = this val layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ) layoutParams.gravity = Gravity.CENTER_HORIZONTAL findViewById(R.id.banner_container).addView(bannerAd, layoutParams) } // CloudXAdViewListener callbacks override fun onAdLoaded(cloudXAd: CloudXAd) { Log.d("CloudX", "Banner ad loaded from ${cloudXAd.networkName}") } override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) { Log.e("CloudX", "Banner ad failed to load: ${cloudXError.message}") } override fun onAdClicked(cloudXAd: CloudXAd) { Log.d("CloudX", "Banner ad clicked") } override fun onAdExpanded(cloudXAd: CloudXAd) { Log.d("CloudX", "Banner ad expanded") } override fun onAdCollapsed(cloudXAd: CloudXAd) { Log.d("CloudX", "Banner ad collapsed") } // CloudXAdRevenueListener callback override fun onAdRevenuePaid(cloudXAd: CloudXAd) { Log.d("CloudX", "Banner bid-time revenue: ${cloudXAd.revenue} from ${cloudXAd.networkName}") } override fun onDestroy() { super.onDestroy() bannerAd.destroy() } } ``` ```java Java public class MainActivity extends AppCompatActivity implements CloudXAdViewListener, CloudXAdRevenueListener { private CloudXAdView bannerAd; private void createBannerAd() { bannerAd = CloudX.createBanner(this, "your-banner-ad-unit-id"); bannerAd.setListener(this); bannerAd.setRevenueListener(this); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ); layoutParams.gravity = Gravity.CENTER_HORIZONTAL; ((LinearLayout) findViewById(R.id.banner_container)).addView(bannerAd, layoutParams); } @Override protected void onDestroy() { super.onDestroy(); if (bannerAd != null) { bannerAd.destroy(); } } // CloudXAdViewListener callbacks @Override public void onAdLoaded(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Banner ad loaded from " + cloudXAd.getNetworkName()); } @Override public void onAdLoadFailed(@NonNull String adUnitId, @NonNull CloudXError cloudXError) { Log.e("CloudX", "Banner ad failed to load: " + cloudXError.getMessage()); } @Override public void onAdClicked(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Banner ad clicked"); } @Override public void onAdExpanded(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Banner ad expanded"); } @Override public void onAdCollapsed(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Banner ad collapsed"); } // CloudXAdRevenueListener callback @Override public void onAdRevenuePaid(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Banner bid-time revenue: " + cloudXAd.getRevenue() + " from " + cloudXAd.getNetworkName()); } } ``` Banner ads auto-refresh by default unless disabled in CloudX configuration. With SDK 4.7.0 or later, `startAutoRefresh()` cannot override that setting. Use `load()` for manual requests on a server-disabled ad unit. A pause made with `stopAutoRefresh()` can still be resumed. To control refresh manually: ```kotlin Kotlin bannerAd.stopAutoRefresh() // Stop auto-refresh bannerAd.load() // Manually load a new ad bannerAd.startAutoRefresh() // Re-enable auto-refresh ``` ```java Java bannerAd.stopAutoRefresh(); // Stop auto-refresh bannerAd.load(); // Manually load a new ad bannerAd.startAutoRefresh(); // Re-enable auto-refresh ``` Optional placement and custom data for tracking: ```kotlin Kotlin bannerAd.setPlacement("home_screen") bannerAd.setCustomData("level:5,coins:100") ``` ```java Java bannerAd.setPlacement("home_screen"); bannerAd.setCustomData("level:5,coins:100"); ``` ## MREC Ads (300x250) ```kotlin Kotlin class MainActivity : AppCompatActivity(), CloudXAdViewListener, CloudXAdRevenueListener { private lateinit var mrecAd: CloudXAdView private fun createMrecAd() { mrecAd = CloudX.createMREC(this, "your-mrec-ad-unit-id") mrecAd.listener = this mrecAd.revenueListener = this val layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ) layoutParams.gravity = Gravity.CENTER_HORIZONTAL findViewById(R.id.mrec_container).addView(mrecAd, layoutParams) } // CloudXAdViewListener callbacks (same as Banner) override fun onAdLoaded(cloudXAd: CloudXAd) { Log.d("CloudX", "MREC ad loaded from ${cloudXAd.networkName}") } override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) { Log.e("CloudX", "MREC ad failed to load: ${cloudXError.message}") } override fun onAdClicked(cloudXAd: CloudXAd) { Log.d("CloudX", "MREC ad clicked") } override fun onAdExpanded(cloudXAd: CloudXAd) { Log.d("CloudX", "MREC ad expanded") } override fun onAdCollapsed(cloudXAd: CloudXAd) { Log.d("CloudX", "MREC ad collapsed") } // CloudXAdRevenueListener callback override fun onAdRevenuePaid(cloudXAd: CloudXAd) { Log.d("CloudX", "MREC bid-time revenue: ${cloudXAd.revenue} from ${cloudXAd.networkName}") } override fun onDestroy() { super.onDestroy() mrecAd.destroy() } } ``` ```java Java public class MainActivity extends AppCompatActivity implements CloudXAdViewListener, CloudXAdRevenueListener { private CloudXAdView mrecAd; private void createMrecAd() { mrecAd = CloudX.createMREC(this, "your-mrec-ad-unit-id"); mrecAd.setListener(this); mrecAd.setRevenueListener(this); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ); layoutParams.gravity = Gravity.CENTER_HORIZONTAL; ((LinearLayout) findViewById(R.id.mrec_container)).addView(mrecAd, layoutParams); } @Override protected void onDestroy() { super.onDestroy(); if (mrecAd != null) { mrecAd.destroy(); } } // CloudXAdViewListener callbacks (same as Banner) @Override public void onAdLoaded(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "MREC ad loaded from " + cloudXAd.getNetworkName()); } @Override public void onAdLoadFailed(@NonNull String adUnitId, @NonNull CloudXError cloudXError) { Log.e("CloudX", "MREC ad failed to load: " + cloudXError.getMessage()); } @Override public void onAdClicked(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "MREC ad clicked"); } @Override public void onAdExpanded(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "MREC ad expanded"); } @Override public void onAdCollapsed(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "MREC ad collapsed"); } // CloudXAdRevenueListener callback @Override public void onAdRevenuePaid(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "MREC bid-time revenue: " + cloudXAd.getRevenue() + " from " + cloudXAd.getNetworkName()); } } ``` MREC ads also auto-refresh by default. Use the same refresh control methods as Banner ads. --- # Interstitial Ads > Integrate CloudX interstitial ads in Android apps #### CloudX-rendered creatives CloudX-served creatives are rendered by the built-in renderer, which ships inside the core SDK — no separate renderer adapter module is required. The creative type is selected automatically from the bid response, so no publisher integration is needed to render either format. The renderer supports MRAID 3.0 (HTML) and VAST creatives across the ad formats: | Ad format | HTML (MRAID 3.0) | VAST | | ------------ | ---------------- | ---- | | Banner | ✓ | — | | MREC | ✓ | ✓ | | Interstitial | ✓ | ✓ | | Rewarded | ✓ | ✓ | ```kotlin Kotlin class MainActivity : AppCompatActivity(), CloudXInterstitialListener, CloudXAdRevenueListener { private lateinit var interstitialAd: CloudXInterstitialAd private var retryAttempt = 0 private fun createInterstitialAd() { interstitialAd = CloudX.createInterstitial(this, "your-interstitial-ad-unit-id") interstitialAd.listener = this interstitialAd.revenueListener = this interstitialAd.load() } private fun showInterstitialAd() { if (interstitialAd.isAdReady) { // Basic show interstitialAd.show(this) // Or with optional placement and custom data for tracking // interstitialAd.show(this, "level_complete", "level:5,score:1000") } else { Log.w("CloudX", "Interstitial ad not ready yet") } } // CloudXInterstitialListener callbacks override fun onAdLoaded(cloudXAd: CloudXAd) { Log.d("CloudX", "Interstitial ad loaded from ${cloudXAd.networkName}") retryAttempt = 0 } /** * Retries loading with exponential backoff: delay doubles with each attempt * (2s → 4s → 8s … 32s max) to avoid hammering the server during transient * failures. [retryAttempt] resets to 0 in [onAdLoaded]. */ override fun onAdLoadFailed(adUnitId: String, cloudXError: CloudXError) { Log.e("CloudX", "Interstitial ad failed to load: ${cloudXError.message}") retryAttempt++ val delayMillis = TimeUnit.SECONDS.toMillis(2.0.pow(retryAttempt.coerceAtMost(5)).toLong()) Handler(Looper.getMainLooper()).postDelayed({ interstitialAd.load() }, delayMillis) } override fun onAdDisplayed(cloudXAd: CloudXAd) { Log.d("CloudX", "Interstitial ad displayed") } override fun onAdDisplayFailed(cloudXAd: CloudXAd, cloudXError: CloudXError) { Log.e("CloudX", "Interstitial ad failed to display: ${cloudXError.message}") interstitialAd.load() } override fun onAdHidden(cloudXAd: CloudXAd) { Log.d("CloudX", "Interstitial ad hidden") // Reload for next use interstitialAd.load() } override fun onAdClicked(cloudXAd: CloudXAd) { Log.d("CloudX", "Interstitial ad clicked") } // CloudXAdRevenueListener callback override fun onAdRevenuePaid(cloudXAd: CloudXAd) { Log.d("CloudX", "Interstitial bid-time revenue: ${cloudXAd.revenue} from ${cloudXAd.networkName}") } override fun onDestroy() { super.onDestroy() interstitialAd.destroy() } } ``` ```java Java public class MainActivity extends AppCompatActivity implements CloudXInterstitialListener, CloudXAdRevenueListener { private CloudXInterstitialAd interstitialAd; private int retryAttempt; private void createInterstitialAd() { interstitialAd = CloudX.createInterstitial(this, "your-interstitial-ad-unit-id"); interstitialAd.setListener(this); interstitialAd.setRevenueListener(this); interstitialAd.load(); } private void showInterstitialAd() { if (interstitialAd.isAdReady()) { // Basic show interstitialAd.show(this); // Or with optional placement and custom data for tracking // interstitialAd.show(this, "level_complete", "level:5,score:1000"); } else { Log.w("CloudX", "Interstitial ad not ready yet"); } } @Override protected void onDestroy() { super.onDestroy(); if (interstitialAd != null) { interstitialAd.destroy(); } } // CloudXInterstitialListener callbacks @Override public void onAdLoaded(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Interstitial ad loaded from " + cloudXAd.getNetworkName()); retryAttempt = 0; } @Override public void onAdLoadFailed(@NonNull String adUnitId, @NonNull CloudXError cloudXError) { Log.e("CloudX", "Interstitial ad failed to load: " + cloudXError.getMessage()); retryAttempt++; long delayMillis = TimeUnit.SECONDS.toMillis((long) Math.pow(2, Math.min(5, retryAttempt))); new Handler(Looper.getMainLooper()).postDelayed(() -> interstitialAd.load(), delayMillis); } @Override public void onAdDisplayed(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Interstitial ad displayed"); } @Override public void onAdDisplayFailed(@NonNull CloudXAd cloudXAd, @NonNull CloudXError cloudXError) { Log.e("CloudX", "Interstitial ad failed to display: " + cloudXError.getMessage()); interstitialAd.load(); } @Override public void onAdHidden(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Interstitial ad hidden"); // Reload for next use interstitialAd.load(); } @Override public void onAdClicked(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Interstitial ad clicked"); } // CloudXAdRevenueListener callback @Override public void onAdRevenuePaid(@NonNull CloudXAd cloudXAd) { Log.d("CloudX", "Interstitial bid-time revenue: " + cloudXAd.getRevenue() + " from " + cloudXAd.getNetworkName()); } } ``` #### Expired ads A loaded ad that is not shown before it expires is reloaded automatically on the same ad object. The current ad stays ready and can still be shown until the replacement is ready. * To be told when a replacement is ready, set an `expirationListener`. A replacement that loads on its own does not call `onAdLoaded` again. * If you show the ad while the replacement loads, the replacement is kept. It becomes ready after the ad closes, and the `expirationListener` is called then. * If you call `load()` while the replacement is loading, including a `load()` from `onAdHidden`, no second request starts. You receive `onAdLoaded` with the new ad, followed by `onExpiredAdReloaded`. * If the replacement fails to load while the expired ad is still loaded, `onAdLoadFailed` fires and the ad is no longer ready. Call `load()` again, after a delay rather than in a tight loop. ```kotlin Kotlin interstitialAd.expirationListener = CloudXAdExpirationListener { expiredAd, newAd -> Log.d("CloudX", "Expired interstitial ad replaced by an ad from ${newAd.networkName}") } ``` ```java Java interstitialAd.setExpirationListener((expiredAd, newAd) -> Log.d("CloudX", "Expired interstitial ad replaced by an ad from " + newAd.getNetworkName())); ``` --- # Native Ads > Load and render CloudX native ads in custom Android layouts Native ads expose individual creative assets—such as the title, body, icon, media, and call to action—so you can render them in a layout that matches your app. CloudX populates the bound views and delegates impression and click tracking to the winning network adapter. The integration has three steps: 1. Bind your layout's UI components. 2. Create a loader and load or render the native ad. 3. Destroy the ad and loader when they are no longer needed. > **Note:** This page covers standalone Native ad units loaded with `CloudXNativeAdLoader`. Native Banner and Native MREC creatives served in Banner or MREC slots use the regular [`CloudXAdView` integration](/en/android/ad-formats/banner-mrec); they do not use a publisher-provided native layout or binder. ## Supported networks Per-network Native support — including the Native Banner and Native MREC variants — is maintained in the [Native ad format matrix](/en/ad-formats/native). Adapter-specific dependencies and setup notes are documented on each adapter's overview page. ## 1. Bind UI components Create a layout containing the native assets you want to display, then map each view ID with `CloudXNativeAdViewBinder`. A network may omit optional assets, so make sure your layout can collapse or hide empty views. > **Warning:** Networks require their key assets — most importantly the media container bound with `setMediaContentViewGroupId(...)` — to be rendered and visible on screen, or they do not count the impression. The ad still displays, so the revenue loss is silent. Follow each network's asset requirements on its adapter overview page, linked from the [Native ad format matrix](/en/ad-formats/native). The options container is required: CloudX uses it to display the network-provided AdChoices or privacy control. ```xml res/layout/native_ad_layout.xml