From 9da0a9beba0c0fc5ed77145a47ecb1b14d3a2f55 Mon Sep 17 00:00:00 2001
From: Anton Urankar <124867229+antonurankar-moloco@users.noreply.github.com>
Date: Mon, 14 Sep 2026 17:01:34 +0200
Subject: [PATCH] Rewrite the README for a publisher who has never seen this
code
The old README explained the integration before it let anyone run it: the
Running section sat at line 167, after every rationale, and it never said
which buttons drive the demo. It also never stated the thing that makes the
first five minutes work, that the checked-in ids are CloudX's public sample
app, so a clone runs with no account and no setup.
Restructured so the order matches what a stranger does: see it, run it,
understand it, adopt it. The screenshots move to the top, Run it becomes the
second section and names the two buttons, and Required setup becomes Point
it at your own app, since none of it is required to see the demo work.
Dropped the parts written for a reviewer rather than a reader: the
`grep -rn "CloudX\.\|MobileAds\."` proof of UI separation and the paragraph
listing which framework imports each integration file uses. The claim those
supported is kept. lib/ui and lib/main.dart move out of the copy-this table,
which was telling readers to copy a file and ignore it in the same row.
Added a Getting help section, which did not exist, and framed the four cycle
gotchas as things to understand before adapting the code rather than as
loose details. Links now point at /en/flutter/integration directly, because
/en/flutter is a 307 to it.
---
README.md | 153 ++++++++++++++++++++++++++++++++----------------------
1 file changed, 90 insertions(+), 63 deletions(-)
diff --git a/README.md b/README.md
index f50cd02..ea61cdf 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,44 @@
# CloudX Trusted Arbiter Demo - Flutter
-A working Trusted Arbiter integration for interstitials. CloudX and AdMob load
-in parallel, both fills become bids, and `CloudX.arbiter` decides which one is
-shown. Companion to [docs.cloudx.io](https://docs.cloudx.io/en/flutter).
+A complete, runnable Flutter app showing how to put CloudX in competition with
+another ad network on price. CloudX and AdMob load an interstitial in parallel,
+both fills become bids, and `CloudX.arbiter` decides which one gets shown.
-Rewarded ads follow the same flow with one extra callback. Banners are a
-different shape entirely: `CloudX.createBanner` places a view at a fixed screen
-position and refreshes it on its own, with no arbiter round. Neither is
-repeated here; interstitial is the whole demo.
+Clone it and run it. It works out of the box against CloudX's public sample app,
+so you can watch a real auction before you change a single id.
+
+
+
+
+Interstitials only. Rewarded ads follow the same flow with one extra callback,
+and banners are a different shape entirely (`CloudX.createBanner` places a view
+at a fixed position and refreshes it on its own, with no arbiter round), so
+neither is repeated here.
+
+Full documentation: [Flutter integration
+guide](https://docs.cloudx.io/en/flutter/integration) and [Trusted
+Arbiter](https://docs.cloudx.io/en/flutter/trusted-arbiter).
+
+## Run it
+
+You need Flutter 3.44 or newer (Dart 3.12), and Xcode if you want the iOS side.
+Nothing else: the ids checked in belong to CloudX's public sample app, so the
+demo runs as-is.
+
+```sh
+flutter pub get
+(cd ios && pod install) # iOS only
+flutter run
+```
+
+Tap **Load both**, wait for both sides to settle, then tap **Show winner**. On
+iOS you will be asked for tracking permission first; see the ATT note under
+[Point it at your own app](#point-it-at-your-own-app) for why that has to come
+before anything else.
+
+If you want the iOS build on a device or an archive, set your own Signing Team
+in Xcode. The project deliberately ships with no `DEVELOPMENT_TEAM`, so signing
+stays on automatic and resolves to your team rather than ours.
## What Trusted Arbiter is
@@ -16,7 +47,7 @@ demand compete against CloudX on price instead of sitting in a waterfall above
or below it: you load both, hand both to CloudX as bids, and CloudX tells you
which one to show.
-Two rules are easy to miss, and both are in the code:
+Two rules are easy to miss, and both are visible in the code:
> **The arbiter runs before the show, not during it.** Both sides load, the
> arbiter picks a winner, and the winner is stored. The tap that shows an ad
@@ -34,11 +65,11 @@ AdMob bid therefore reaches the arbiter with no price at all. Expect CloudX to
lose those rounds. Point the demo at a real AdMob unit that pays to see prices
compete.
-## What is in here
+## What to copy into your app
-Copy [`lib/cloudx/`](lib/cloudx). Those six files are the whole integration,
-and none of them builds a widget. Everything outside that folder is this demo's
-own scaffolding.
+Copy [`lib/cloudx/`](lib/cloudx). Those six files are the whole integration, and
+none of them builds a widget, so they drop into an app with any UI. Everything
+outside that folder is this demo's own scaffolding.
| File | What it is |
|---|---|
@@ -48,10 +79,11 @@ own scaffolding.
| [`lib/cloudx/tracking_gate.dart`](lib/cloudx/tracking_gate.dart) | The iOS App Tracking Transparency gate. |
| [`lib/cloudx/cloudx_failure_text.dart`](lib/cloudx/cloudx_failure_text.dart) | One line out of a CloudX failure, carrying the SDK's own name for the error code. |
| [`lib/cloudx/demo_config.dart`](lib/cloudx/demo_config.dart) | App key and ad unit ids, per platform. The first file to edit. |
-| [`lib/ui/arbiter_screen.dart`](lib/ui/arbiter_screen.dart) | Demo-only UI. Ignore it when reading the integration. |
-| [`lib/main.dart`](lib/main.dart) | `runApp`, nothing else. |
-**Take fewer and it will not build.** The controller reports through
+`lib/ui/` and `lib/main.dart` are the demo's screen and entry point. They are
+here so the app runs; they are not part of the integration.
+
+**Take fewer files and it will not build.** The controller reports through
`arbiter_events.dart`, both it and `sdk_startup.dart` format failures through
`cloudx_failure_text.dart`, and `sdk_startup.dart` calls `tracking_gate.dart`.
If your app already initializes CloudX and answers the ATT prompt, drop
@@ -66,14 +98,7 @@ Read them in this order:
3. [`arbiter_interstitial_controller.dart`](lib/cloudx/arbiter_interstitial_controller.dart)
- the cycle itself
-**No CloudX or AdMob call lives outside `lib/cloudx/`.** That is checkable:
-`grep -rn "CloudX\.\|MobileAds\." lib/ui lib/main.dart` returns only two
-strings printed on screen. The integration files import
-`package:flutter/foundation.dart` for logging, and `tracking_gate.dart` also
-needs `package:flutter/widgets.dart` to wait for the app to become active before
-prompting, but none of them imports `material.dart` or builds a widget.
-
-## The cycle
+## How the cycle works
```
load()
@@ -89,7 +114,8 @@ prompting, but none of them imports `material.dart` or builds a widget.
ad closes --> that side's fill is consumed --> load() reloads only what is missing
```
-Details worth knowing:
+Four things in that cycle are easy to get wrong. The code handles all four, and
+they are worth understanding before you adapt it:
- **A `none` result is not a winner.** It is stored as "nothing", so the next
`load()` runs the round again instead of parking on a winner that cannot show.
@@ -104,15 +130,21 @@ Details worth knowing:
`LOAD_NOT_ALLOWED_WHILE_SHOWING`. Destroying leaves the next load to build a
fresh instance, which is also what makes it run a new auction.
-## Required setup
+## Point it at your own app
-1. **Enable Trusted Arbiter for your app in the CloudX dashboard.** No code here
- can turn it on. You can confirm it from the logs at startup:
+The ad unit ids checked in here belong to CloudX's public sample app
+(`io.cloudx.sample`), and the AdMob ids are Google's public test units. Replace
+all of them, and do these five things together:
+
+1. **Ask CloudX to enable Trusted Arbiter for your app.** No code here can turn
+ it on. Confirm it from the logs at startup:
`[InitializationService] Arbiter enabled: https://sdk.cloudx.io/arbitration`.
+ If that line is missing, the arbiter call will not do what this demo shows.
2. **Match your app key to your bundle id.** Bid requests are authorized per app
key AND bundle id. Change `lib/cloudx/demo_config.dart`, the Android
- `applicationId` and the iOS `PRODUCT_BUNDLE_IDENTIFIER` together, or every
- round comes back `NO_FILL[302]` with nothing to say the pairing is why.
+ `applicationId` and the iOS `PRODUCT_BUNDLE_IDENTIFIER` together. Get this
+ wrong and every round comes back `NO_FILL[302]`, with nothing on screen to
+ say the pairing is the reason.
3. **Set the Google Mobile Ads application id natively**, in
`android/app/src/main/AndroidManifest.xml` and `ios/Runner/Info.plist`. The
Google SDK throws at startup when it is absent.
@@ -125,15 +157,25 @@ Details worth knowing:
The prompt is answered while `CloudX SDK` still reads `not initialized`. That
order is the point: initialize first and every request that session goes out
without an IDFA and with `dnt = 1`.
-5. **Set your Signing Team in Xcode** before an iOS device or archive build. The
- project ships with no `DEVELOPMENT_TEAM` on purpose, so signing stays on
- automatic and resolves to your own team.
+5. **Set your Signing Team in Xcode** before an iOS device or archive build, as
+ above.
+
+## Reading the screen
+
+Every status line names the platform it came from, so the screen doubles as the
+diagnostic.
+
+| Line | Meaning |
+|---|---|
+| `CloudX` / `AdMob` | That side's last event: `loading`, `loaded: $`, `load failed: ...`, `showing`, `closed`. A CloudX failure carries the SDK's own name for the code, so a round that did not fill reads `load failed: No ad available. (NO_FILL[302])` rather than a bare number. |
+| `Arbiter` | `ADMOB (2 bids)`, `CLOUDX (2 bids)`, `no winner (1 bid)`, or `failed: ...`. |
+| `Revenue -> CloudX` | The last AdMob paid event forwarded through `reportRevenueData`, and what that call returned. A `true` does not mean the price was kept; a revenue of 0.0 is discarded. |
-> The ad unit ids checked in here belong to the public CloudX sample app
-> (`io.cloudx.sample`), and the AdMob ids are Google's public test units.
-> Replace all of them.
+If `Arbiter` only ever reads `(1 bid)`, one side is not filling. Look at which
+of the two lines above it says `load failed`; the arbiter is working correctly
+either way.
-## Versions
+## Versions and adapters
| Pin | Version | Why |
|---|---|---|
@@ -146,16 +188,16 @@ Details worth knowing:
Dart 3.12 and Flutter 3.44 are the floors, set by `webview_flutter_android` and
`webview_flutter_wkwebview`, which `google_mobile_ads` 9.1.0 pulls in.
+The adapter list in `android/app/build.gradle.kts` and `ios/Podfile` is the full
+CloudX set, so you can see the shape of it. **Take only the networks your
+dashboard actually serves**; each one adds to your binary.
+
**`CloudXGoogleWaterfallAdapter` / `io.cloudx:adapter-googlewaterfall` is
deliberately absent.** It runs AdMob demand *inside* the CloudX auction, which
is the opposite of what this demo shows: here AdMob is an external bid competing
against CloudX through the arbiter. Shipping both would make the two bids the
same demand.
-The adapter list in `android/app/build.gradle.kts` and `ios/Podfile` is the full
-CloudX set. Take only the networks your dashboard actually serves; each one adds
-to your binary.
-
**BIGO is Android only**, which is why the Gradle file lists one network more
than the Podfile. It also needs cleartext traffic to `127.0.0.1`, because the
BIGO Ads SDK serves some creative assets from a loopback server on the device:
@@ -164,28 +206,13 @@ and the `` element references it. Drop the adapter and you can drop
both. See the
[BIGO adapter page](https://docs.cloudx.io/en/android/adapters/bigo/overview).
-## Running
+## Getting help
-```sh
-flutter pub get
-(cd ios && pod install) # iOS only
-flutter run
-```
+Start with the [Flutter integration
+guide](https://docs.cloudx.io/en/flutter/integration), the [Trusted Arbiter
+page](https://docs.cloudx.io/en/flutter/trusted-arbiter), and the
+[changelog](https://docs.cloudx.io/en/flutter/changelog). The plugin itself is
+on pub.dev as [`cloudx_flutter`](https://pub.dev/packages/cloudx_flutter).
-## Reading the screen
-
-Every status line names the platform it came from, so the screen doubles as the
-diagnostic.
-
-
-
-
-| Line | Meaning |
-|---|---|
-| `CloudX` / `AdMob` | That side's last event: `loading`, `loaded: $`, `load failed: ...`, `showing`, `closed`. A CloudX failure carries the SDK's own name for the code, so a round that did not fill reads `load failed: No ad available. (NO_FILL[302])` rather than a bare number. |
-| `Arbiter` | `ADMOB (2 bids)`, `CLOUDX (2 bids)`, `no winner (1 bid)`, or `failed: ...`. |
-| `Revenue -> CloudX` | The last AdMob paid event forwarded through `reportRevenueData`, and what that call returned. A `true` does not mean the price was kept; a revenue of 0.0 is discarded. |
-
-If `Arbiter` only ever reads `(1 bid)`, one side is not filling. Look at which
-of the two lines above it says `load failed`; the arbiter is working correctly
-either way.
+For an app key, ad unit ids, or to have Trusted Arbiter switched on, talk to
+your CloudX contact.