●DEADLINE — Four days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alike●RULES — The submission rule and the visibility rule are separate. An app you have stopped updating still disappears for new users on newer devices if it targets below API 35●EXTENSION — An extension keeps you shipping to all users until November 1, but the form lives in Play Console and has to be filed before the deadline passes●EXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 while React stays at 19.2, and 0.86 is intended to land without breaking changes●HERMES — 57.0.9 updates React Native to 0.86.2 and clears the Hermes V1 memory regression from SDK 56, which shows up in apps importing reanimated or worklets●PREBUILD — expo prebuild now clears and regenerates the native android and ios directories by default, so hand-edited native changes vanish unless you audit for them first●DEADLINE — Four days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alike●RULES — The submission rule and the visibility rule are separate. An app you have stopped updating still disappears for new users on newer devices if it targets below API 35●EXTENSION — An extension keeps you shipping to all users until November 1, but the form lives in Play Console and has to be filed before the deadline passes●EXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 while React stays at 19.2, and 0.86 is intended to land without breaking changes●HERMES — 57.0.9 updates React Native to 0.86.2 and clears the Hermes V1 memory regression from SDK 56, which shows up in apps importing reanimated or worklets●PREBUILD — expo prebuild now clears and regenerates the native android and ios directories by default, so hand-edited native changes vanish unless you audit for them first
AdMob Mediation Dropped My eCPM by 30% After I Set It Up — What Went Wrong and How I Fixed It
My eCPM dropped 30% the morning after enabling AdMob mediation. Drawing on years of indie monetization work, here are the pitfalls I found in Rork apps, how to verify adapters are actually alive, how to derive floors from real data, and how to decide whether mediation is worth it at all.
The morning after setting up AdMob mediation, I opened the dashboard and froze.
My eCPM had dropped 30%. I had set up mediation specifically because "competing ad networks drive prices up" — yet somehow the result was the opposite. A cold, sinking feeling I still remember. I've been building apps independently since 2014, and very few monetization experiments have made me feel as much like I'd made a mistake.
When you work on indie app monetization for long enough, the volume of trial and error around revenue keeps growing. From that experience, mediation failures right after setup are remarkably common — and Rork apps have a few specific pitfalls that make them especially prone to it.
In this article I'll walk through the failure and recovery in order: why eCPM drops, the pitfalls specific to Rork, how to verify your adapters are actually running, how to derive floors from measured data, how to measure the effect with real numbers, and finally how to decide whether you should adopt mediation at all.
What Was Actually Happening
Mediation works by letting multiple ad networks (AppLovin, Unity Ads, Meta Audience Network, etc.) compete for each impression, theoretically pushing eCPM higher. When it works, it works well. The key phrase is "when it works."
Looking back, my eCPM drop had a clear cause: the waterfall configuration was wrong. In situations where AdMob should have been called first, lower-CPM networks were responding first instead.
When you implement ads in a Rork app, the AI-generated code handles the basic AdMob setup reliably. Mediation adapter configuration, though, requires a lot of work in the AdMob console outside of the code — and that's where things tend to go sideways.
Why Mediation Can Actually Lower Your eCPM
"Competition should push prices up," yet it went down. To understand the contradiction, you need to look at how waterfall mediation works internally.
In a waterfall, each network gets a priority (an eCPM floor), and AdMob asks them one by one, top to bottom: "Can you serve an ad at this rate?" As soon as a network answers, the auction stops there and lower networks are never asked.
That's where the trap is. If a network with a low floor sits near the top, it fills an impression cheaply that could have sold for much more. In auction terms, you're selling to the first person who raises their hand instead of waiting for the highest bid.
In my case, a network with the default $0.50 floor had effectively become the "instant closer." Impressions that should have sold for $1.80–$2.20 were being cleared around $0.50, one after another. That was the real source of the 30% drop.
In other words, mediation isn't something that raises revenue just by being enabled. It only works once each network's floor matches reality. Skip that step, and adding networks becomes a hole in your revenue.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A three-step method for setting waterfall floors from the 40th percentile of real eCPM
✦An adapter health check you run at launch, with the correct consent-to-init ordering
✦A week-over-week threshold alert that catches an eCPM drop the same night
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Pitfall 1: Leaving Waterfall eCPM Floors at Default Values
When you configure mediation in the AdMob console, did you leave each network's eCPM floor at the default value?
Google's recommended Bidding (real-time bidding) approach handles optimization automatically, but some networks still only support waterfall mediation. For those, you need to set eCPM floors manually against your actual display rates.
My mistake was leaving the default $0.50 floor in place. My actual eCPM was running between $1.80 and $2.20, so low-quality $0.50-floor ads were flooding my impressions. After adjusting the floor to $1.50, eCPM recovered within three days.
Pitfall 2: ATT and Ad ID Disconnect on iOS
When I added ATT (App Tracking Transparency) to an existing Rork project after the fact, the mediation adapters ended up in a state where they couldn't receive the advertising identifier.
The initialization order matters. If you initialize mediation adapters before the user has responded to the ATT prompt, iOS 14+ will run them without an advertising ID — meaning untargeted ads run for those users indefinitely.
If you also serve the EU, there's a stage before that: UMP (the consent management platform). The correct sequence is UMP consent → ATT prompt → SDK initialization. Collapsing all three into a single async function is the shape I eventually settled on.
import { Platform } from 'react-native';import { requestTrackingPermission } from 'react-native-tracking-transparency';import MobileAds, { AdsConsent } from 'react-native-google-mobile-ads';// ✅ One function that guarantees UMP consent → ATT → SDK initexport const initializeAdsPipeline = async () => { // 1. UMP: the form only appears for EEA/UK users; resolves immediately elsewhere const consentInfo = await AdsConsent.requestInfoUpdate(); if (consentInfo.isConsentFormAvailable) { await AdsConsent.showForm(); } // 2. ATT: iOS only. Running it after UMP avoids stacking two dialogs at once if (Platform.OS === 'ios') { const status = await requestTrackingPermission(); console.log('ATT status:', status); } // 3. SDK init: only now do the adapters read the resolved consent state const adapterStatuses = await MobileAds().initialize(); return adapterStatuses;};
When writing Rork prompts, specifying "initialize the AdMob SDK after UMP consent and the ATT request" gets the AI to generate code in the correct order. That said, when adding ATT or UMP to an existing project, don't just paste the generated code — verify the initialization timing manually. I've written up the consent architecture itself in production patterns for UMP and ATT consent management.
Pitfall 3: Not Separating Adapter Configuration by Platform
Rork generates code for iOS and Android simultaneously, but AdMob mediation adapters require separate package IDs per OS. If you don't specify iOS and Android configurations separately in your app.json plugin settings, one platform's adapters won't be recognized and ads simply won't show.
// app.json plugin configuration example (iOS/Android separated){ "plugins": [ [ "react-native-google-mobile-ads", { "androidAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX", "iosAppId": "ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX", "userTrackingUsageDescription": "This identifier will be used to deliver personalized ads to you.", "skAdNetworkItems": [ { "SKAdNetworkIdentifier": "cstr6suwn9.skadnetwork" }, { "SKAdNetworkIdentifier": "4fzdc2evr5.skadnetwork" } ] } ] ]}
The SKAdNetworkItems list changes with every network version update. Building a habit of reviewing it monthly prevents the kind of silent iOS ad impression drops that are easy to miss until you check the data.
One related note: if your iOS build stops compiling right after you add an adapter, the cause is usually native linking rather than this configuration. I walk through that separation in linker errors and missing ads after adding a mediation adapter.
Verifying Your Adapters Are Actually Alive
The biggest lesson from that 30% drop was this: "configured in the console" does not mean "running in the app." A network can be enabled console-side and still fail to initialize on device — and a network that fails to initialize never enters the auction. The more networks sit out, the easier it is for the remaining cheap network to win.
MobileAds().initialize() returns per-adapter status. An awful lot of projects throw that return value away. Mine did too, for a long time.
import MobileAds, { AdapterStatus } from 'react-native-google-mobile-ads';// The adapters you enabled in the AdMob console, to reconcile againstconst EXPECTED_ADAPTERS = [ 'com.google.android.gms.ads.MobileAds', 'com.applovin.mediation.adapters.AppLovinMediationAdapter', 'com.google.ads.mediation.unity.UnityAdapter',];export const verifyAdapters = (statuses: AdapterStatus[]) => { // state: 0 = NOT_READY, 1 = READY const notReady = statuses.filter((s) => s.state !== 1); const missing = EXPECTED_ADAPTERS.filter( (name) => !statuses.some((s) => s.name === name), ); if (notReady.length || missing.length) { // In production, send this to Crashlytics as a non-fatal instead of console console.warn('[ads] adapter check failed', { notReady: notReady.map((s) => `${s.name}: ${s.description}`), missing, }); } // Returning the participation ratio lets you correlate it with eCPM later return { ready: statuses.length - notReady.length, total: statuses.length };};
Don't discard description. That's where you get the actual reason — "SDK version mismatch," "missing app id," and so on. In my case, this log is what finally revealed that I'd forgotten an adapter App ID on one platform only. No amount of staring at the console would have surfaced that.
In production I send the warning to Crashlytics as a non-fatal event and watch the ready / total ratio across releases. When that ratio drops in a release, eCPM drops with it — every time so far.
Deriving Floors From Real Data, in Three Steps
"Match your floors to reality" is easy to write. But what number, exactly? Here's the method I use now.
Pull the last 14 days of impression data, broken out by network and by country
Build the distribution of eCPM that network actually paid, and read the 40th percentile
Use that as your starting floor, then move it ±5% every seven days while watching fill rate
Using a percentile rather than an average is the crucial part. Averages get dragged upward by a handful of high-value impressions, so an average-based floor tends to sit above reality and your fill rate collapses. Go the other way and floor at the minimum, and you've just rebuilt the "instant closer" that cost me 30%. The 40th percentile is where I landed after a fair amount of trial and error.
// Suggest floors from the percentile distribution of per-network eCPMtype Row = { adSource: string; earnings: number; impressions: number };const percentile = (sorted: number[], p: number) => { const idx = Math.floor((sorted.length - 1) * p); return sorted[idx];};export function suggestFloors(rows: Row[], p = 0.4) { const byNetwork = new Map<string, number[]>(); for (const r of rows) { if (r.impressions < 100) continue; // thin days skew the distribution const ecpm = (r.earnings / r.impressions) * 1000; byNetwork.set(r.adSource, [...(byNetwork.get(r.adSource) ?? []), ecpm]); } return [...byNetwork.entries()].map(([adSource, values]) => { const sorted = values.sort((a, b) => a - b); return { adSource, floor: Number(percentile(sorted, p).toFixed(2)), samples: sorted.length, }; });}
That impressions < 100 filter matters more than it looks. eCPM on a day with a handful of impressions swings wildly, and those days distort the whole distribution. I ran without the filter at first and got floor suggestions nearly a dollar too high, which tanked my fill rate.
The other thing people skip: floors should be set per country. The same network clears at multiple times the rate in one market versus another, so a single global floor leaves money on the table in expensive markets and leaves inventory unfilled in cheap ones. On the risks of lowering floors specifically, I wrote up a case where dropping a floor broke my revenue.
Combining Waterfall and Bidding
"Then just put everything on Bidding" is a reasonable reaction — and for networks that support it, consolidating onto Bidding is the right move. In practice, though, some networks can only be connected through a waterfall, so you end up running both.
Here's the split I eventually settled on.
Method
Best for
Setup effort
Floor tuning
Bidding (real-time)
AdMob itself, Meta, AppLovin and other majors
Low (auto-optimized)
Not needed
Waterfall
Smaller networks without Bidding support
High (manual)
Set manually from the 40th percentile of real eCPM
The principle is simple. Put every Bidding-capable network into the real-time auction, then set the waterfall side's floors so they never undercut the Bidding clearing price. That prevents the waterfall from closing impressions cheaply.
Even in a Rork app, this decision lives on the console side and doesn't touch the generated code. If anything, being able to focus on revenue design without worrying about code is one of the quiet advantages of building with an AI builder. I logged how the numbers moved when I widened the Bidding side to five networks in taking AdMob Bidding to production.
A Triage Table for Missing Ads and Falling Rates
Mediation problems scatter across three layers — your code, the console, and network-side approval status — so poking at them randomly usually makes things worse. This is the triage table I keep on hand.
Symptom
Suspect first
How to check
Ads missing on one OS only
Per-platform App IDs in app.json
Is that adapter present in adapterStatuses?
Ads serve, but rates are low
Waterfall floors
Break impressions out by AD_SOURCE and look for skew
Rates low on iOS only
ATT/UMP and init order
ATT opt-in rate and share of non-personalized ads
Low only right after launch
Learning period (a few days)
Wait seven days before judging. Don't keep touching it
One network fills at zero
That network's review or payment setup
App approval status in that network's own dashboard
That last row is last for a reason. I once spent two full days rewriting code and retuning floors before discovering the app was simply still pending review on the network's side. Suspecting things outside your code first is faster more often than you'd think.
And the fourth row: for the first few days, every network is learning how to serve your inventory, and the numbers sag. Reverting your settings in a panic resets that learning and the numbers sag again. Committing to not touching anything for seven days turned out to be the shortcut.
Measuring eCPM Before and After, Yourself
Mediation's effect should never be judged by "it feels like it went up." The only reason I caught the 30% drop was that I was logging eCPM daily.
You can see AdMob reports in the dashboard, but if you want to compare before and after side by side, it pays to build a small routine that pulls daily figures from the AdMob API and keeps them locally. This isn't app-side code — a lightweight script that just fetches revenue data is enough.
// Minimal example: pull daily eCPM from the AdMob Reporting API// Assume accessToken was already obtained via an authenticated google-auth flowasync function fetchDailyEcpm(accountId: string, accessToken: string) { const endpoint = `https://admob.googleapis.com/v1/accounts/${accountId}/mediationReport:generate`; const body = { reportSpec: { dateRange: { startDate: last7Days().start, endDate: last7Days().end }, dimensions: ['DATE', 'AD_SOURCE', 'COUNTRY'], metrics: ['ESTIMATED_EARNINGS', 'IMPRESSIONS'], }, }; const res = await fetch(endpoint, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); // Compute eCPM yourself as earnings / impressions * 1000, broken out per network return res.json();}
The important part is breaking the data out by the AD_SOURCE dimension. When overall eCPM drops, you can see at a glance which network is dragging it down. In my case, that breakdown is exactly how I finally noticed that "only the $0.50-floor network has an abnormally high impression count." Adding COUNTRY gives you the per-country floors from the previous section for free.
Collecting numbers isn't enough on its own — otherwise you end up like me, freezing in front of a dashboard the next morning. The conditions I run now have gotten pretty simple in practice.
eCPM down 15% or more versus the same weekday last week (weekday seasonality makes day-over-day comparisons useless)
Fill rate down 10 points or more (the signal that a floor went up too far)
Any single network's impression share moving 20 points overnight
That third one is precisely the condition that would have caught my 30% drop. With it in place, I'd have known that same evening rather than the next morning. The notification plumbing itself reuses the four-tier setup I described in running AdMob without checking the dashboard daily.
Keeping the numbers every day lets you decide on facts rather than feel. For revenue decisions, that difference is larger than it sounds.
Should You Adopt Mediation, or Skip It?
Having written all of this, I should say plainly: not every app needs mediation. Setup and ongoing operation take real effort, so if your scale doesn't justify it, AdMob on its own is the healthier choice.
Here are my own rough criteria.
While daily impressions are still under a few thousand, revisiting your eCPM floors or ad formats usually has more upside than mediation
If most of your traffic comes from a few major countries, start with Bidding-capable networks only and leave the waterfall for later
Do you have an operating rhythm that lets you revisit manual floors at least once a month? Without it, waterfalls get left alone and decay
Put the other way: if you have meaningful impression volume and can spare a few dozen minutes a month, mediation pays for itself. The dividing line isn't the setup itself — it's whether you'll keep tending it afterward.
What Recovery Looked Like
Two weeks after fixing the configuration, eCPM returned to its pre-mediation baseline — and then climbed about 8% above it. Mediation finally started doing what it was supposed to do.
Looking back, none of what worked was dramatic. Deriving floors from a percentile instead of a guess, checking adapter state at launch instead of trusting the console, and setting thresholds that surface a drop the same day. Three things. Each one you build once, and then it quietly keeps working.
Working with mediation as an indie developer taught me that the initial setup quality has a disproportionate effect on long-term revenue. One thing I appreciate about building with Rork is that faster app development frees up time for exactly this kind of detailed configuration work outside the code. The tool changes where you spend your attention.
The first concrete action you can take today: open your AdMob console and check whether your waterfall eCPM floors reflect reality. Pull the last 14 days of per-network eCPM, read the 40th percentile, and start there. Thank you for reading.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.