◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-02Advanced

Measuring the Real eCPM Yourself: Piping Impression-Level Ad Revenue Into Your Own Analytics

The AdMob dashboard's average eCPM hides which regions and placements actually earn. Here is how I capture per-impression revenue with paidEventListener, normalize it, and pipe it into my own analytics to compute a real eCPM by network, placement, and country.

React Native239AdMob73Mediation7Revenue AnalyticsArchitecture22

✦ Premium Article

This started when I opened the AdMob dashboard to retune the mediation setup for one of the six wallpaper apps I run in parallel — the one whose revenue had plateaued. The per-network average eCPMs were right there. AdMob high, AppLovin middling, Pangle low. So should I trust those numbers and lower Pangle's floor?

I hesitated. What the dashboard shows is "each network's average across every country and every placement, all blended together." My users are about 60% in Japan, with the rest scattered across North America and Southeast Asia. An interstitial and an app-open ad earn wildly different prices. If I look at the blended average, conclude "Pangle is low," and cut it, I might be emptying out exactly the Southeast Asian banner inventory that only Pangle was filling.

Across more than a decade of solo development since 2014, past fifty million cumulative downloads, ad revenue is the one area where I kept "staring at the dashboard and adjusting by gut." This article is the record of throwing that gut feeling away — of pulling per-impression revenue into my own analytics so I could compute a real eCPM by network, placement, and country.

Why the dashboard's average eCPM misleads you

eCPM is "revenue per 1,000 impressions." The dashboard aggregates this per network, per day. That is convenient, but it is far too coarse for deciding mediation priority or floors (minimum accepted prices).

There are three problems. First, regional differences disappear into the average. For the same banner slot, a North American impression often earns several times what a Japanese one does, and several times more than a Southeast Asian one. Second, placement differences disappear. An app-open ad and an article-footer banner command different prices because they capture different amounts of user attention. Third, only the mediation "winner" shows up in dashboard revenue, so the average can't tell you "if I cut this network, who picks up that inventory?"

In other words, I was trying to make a decision that lives in three dimensions — placement x country x network — using the one dimension the dashboard shows me, the per-network average. That mismatch was the root of the bad calls. What I needed was to record how much each individual impression actually earned, inside my own app.

What Impression-Level Ad Revenue returns

AdMob has a mechanism called Impression-Level Ad Revenue (ILRD). Every time an ad is shown, you receive an estimate of that single impression's revenue through an on-device callback. In react-native-google-mobile-ads, the paidEventListener on each ad object handles this.

The callback mainly returns: value (the revenue, but in micros — an integer that is the real amount times one million), a currency code such as "USD" or "JPY", a precision field (ESTIMATED / PUBLISHER_PROVIDED / PRECISE), and the mediation adapter information telling you which network won.

Here is the first trap. Because value is in micros, you only get the amount in currency units after dividing by 1,000,000. And the currency code can come back as USD or as JPY depending on your account settings and the bidding network. Get either of these wrong and your revenue is off by orders of magnitude. I did exactly that, as you'll see below.

✦

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
✦Why the dashboard's average eCPM misleads mediation decisions, and how to capture per-impression revenue with the paidEventListener in react-native-google-mobile-ads (working code)
✦The micros-vs-currency mistake that put my revenue off by a factor of 1,000,000, plus the Before/After normalization that fixed it
✦How to aggregate a true eCPM by network x placement x country and feed it back into bid floors and mediation priority
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.

or
Unlock all articles with Membership →
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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-08-04
Exactly 20 Characters, Still Rejected: Putting Character Counting Behind One Boundary
Emoji and combining marks make your client and your server disagree about length. I measured four counting methods on Node v22 and found 75.6% of naive truncations split a grapheme and 36.1% produced replacement characters over UTF-8. Here is the shared-module design that fixed it.
⬡ Dev Tools2026-08-01
Every Experiment Kept Landing on the Same Devices: Hash Choice and Salt Position, Measured Across a Million IDs
On-device experiment assignment looked fine in isolation, then collapsed the moment two experiments ran side by side. Here is what one million IDs revealed about four hash functions, why the culprit was the concatenation order rather than hash quality, and the implementation I settled on, with the measured numbers.
⬡ Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.