Running AdMob and AppLovin MAX side by side with an Antigravity sub-agent that compares them daily
For the past two months I have been running my wallpaper apps on both AdMob and AppLovin MAX in parallel, and letting Antigravity sub-agents pull eCPM, fill rate and ARPDAU into a single daily comparison. This is an implementation memo from those 90 days, focused on cross-network normalisation and the thresholds I trust an agent with.
For the past two months I have been running the wallpaper apps in my catalogue on both AdMob and AppLovin MAX in parallel, and asking Antigravity sub-agents to roll up the numbers every day. I am Hirokawa, an indie developer who has been shipping on App Store and Google Play since 2014, and AdMob has been the revenue backbone of the business for over a decade. Switching mediation is not something I want to take a casual swing at. At the same time, in some countries and ad units AppLovin MAX has been showing a real edge in the auction. So instead of forcing an early answer, I decided to measure both against the same yardstick every day, and let the agent fleet do the heavy lifting.
This memo covers what I built over those 90 days. I will skip the obvious SDK installation work and focus on the parts that actually carried the decision: how to call the report APIs, how to split work between sub-agents, where to put guardrails, and where I refused to let the agent decide on my behalf.
Why I ran them in parallel and which three numbers I watched
I did not jump straight to a full AppLovin MAX migration because of a past experience with a different mediation SDK: it looked great after launch, then dipped for seasonal reasons, and I had no clean data to justify rolling back. I needed at least three months of side-by-side numbers before I would trust a migration decision.
After narrowing the metrics down, I landed on these three.
eCPM — revenue per thousand impressions, the unit price
Fill rate — the share of ad requests that get filled, a proxy for inventory depth
ARPDAU — ad revenue per daily active user, the view from the user side
eCPM on its own lies to you. Fill rate can quietly slip while eCPM rises, and the totals shrink even though the "winning" network looks better. I made this mistake more than once back when I was running AdMob alone and manually flicking through reports. Looking at all three together is the bare minimum for a serious mediation comparison, and I lock the agent output into that exact column order so I never read it any other way.
Four sub-agents, one responsibility each
Antigravity sub-agents survive much better when each one owns a small responsibility and you glue them together by strings, instead of asking one big agent to do everything. After 90 days the split I am happy with is:
collector-admob: pulls daily data from the AdMob Reporting API and normalises to JSON
collector-applovin: pulls daily data from the AppLovin MAX Reporting API and normalises to JSON
normalizer: collapses country, currency and ad-unit name differences into a single comparable table
comparator: computes deltas across the three metrics and writes the notification copy when thresholds are crossed
Each one is its own .agent file and the Orchestrator calls them in order. Only the comparator depends on the other three, declared explicitly in the dependencies section of AGENTS.md, so if collector-applovin fails the comparator can still run a one-legged check the next day using yesterday's cached AdMob data. I tried to do this with one big Orchestrator first and ate two full re-runs after report API timeouts before retreating to the split layout.
✦
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 concrete implementation that pulls AdMob and AppLovin MAX reports in parallel from Antigravity sub-agents and normalises by country and ad unit
✦Threshold design that watches eCPM, fill rate and ARPDAU together, and the boundary at which the agent escalates to a human
✦How a solo developer running six apps in parallel built up 90 days of evidence before deciding which units to migrate to AppLovin MAX
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.
On the AdMob side I use googleads-nodejs-client. The two things that bite people the most are the service account scopes and the report granularity, so the core of collector-admob looks like this:
The crucial bit is pinning currencyCode: "USD". AdMob lets you switch currency per call, but I also lock AppLovin MAX to USD, so any FX drift stays out of the comparison signal. Around 40% of my users are domestic in Japan so it would be tempting to look at yen directly, but currency noise is bad for daily deltas. I make the call in USD and bolt yen on at the presentation layer.
The second pragmatic choice is using MATCH_RATE as the fill-rate proxy. AdMob's docs draw a careful line between fill rate and match rate, but when you actually want to compare what got served after mediation, match rate is closer to the field-level intuition.
Pulling AppLovin MAX reports and matching units
AppLovin MAX exposes its reports as CSV with a slightly different shape than AdMob. collector-applovin first pulls CSV, then turns it into JSON that mirrors the AdMob schema:
The MAX report column fill_rate ships as a percentage, while internally I store ratios in the 0–1 range. Forgetting that division by 100 produced a beautiful bug in the first iteration where the comparator believed AdMob was crushing it because MAX's fill rate looked like "75 versus 0.78". This kind of unit mismatch is, by a wide margin, the most common source of cross-network analysis errors.
Absorbing naming drift in the normalizer
Even when the underlying ad unit is the same, the names registered in AdMob and in AppLovin MAX rarely match. AdMob might be carrying Wallpaper_App_iOS_Native_Home while MAX has the same unit registered as home_native_ios. Without flattening, you cannot compare them.
The normalizer agent uses a static alias table that projects both into a canonical key:
The lesson from 90 days here is: do not let the agent "guess" similar units. Antigravity is happy to fuzzy-match, and it will confidently merge two units that look alike but are actually different, which makes the revenue verdict swing wildly. With six apps in production I have roughly 50 ad units to manage, and the alias map still fits in about 300 hand-written lines.
A threshold design that watches three metrics at once
Once the normalizer emits its merged table, the comparator writes the daily verdict. The thresholds I settled on are:
eCPM gap: AppLovin MAX wins as a candidate if it is +10% or more above AdMob.
Fill-rate gap: if AppLovin MAX is more than -5% below AdMob on fill rate, MAX is disqualified from the "winning" bucket regardless of eCPM.
ARPDAU gap: the migration recommendation only fires when the 7-day moving average ARPDAU gap is +5% or more in MAX's favour.
The fill-rate guardrail is the part that actually carries the design. eCPM can rise while real revenue falls; ARPDAU plus fill rate together stop that lie. The core comparator looks like this:
The logic itself is unremarkable. What matters is that it runs with the same rules every day. When I used to do this by hand, my conclusion drifted depending on which units I happened to look at first, or how the day was going. Letting the agent apply a fixed rule every morning is what makes seasonality, weekday effects and small unit-level wobbles add up into something you can read.
What I escalate to a human and what I deliberately do not
Every daily summary lands in a dedicated Slack channel, but I quickly learned that pushing everything in just buries the signal. Antigravity's comparator now prioritises notifications only when one of these is true:
the same (country, adUnit) pair has the same winner for seven consecutive days
ARPDAU swings more than ±15% week over week
either collector has failed twice in a row
When I first dumped every comparison into Slack I stopped reading the channel. Cutting the daily noise to "top three units" plus "up to three warnings" pulled my reaction time on real anomalies from about three days back down to half a day. With AI agents, reducing output volume earns more than adding more checks.
There is one boundary I deliberately keep on the human side: the agent only proposes migration candidates. It does not disable AdMob units or change MAX waterfalls on its own. Pausing an ad SDK has staged rollout and release timing implications that I am not willing to delegate to an agent. That call partly reflects running six apps solo; at a different scale I might draw the line elsewhere.
What the 90 days actually showed and what I decided
A few patterns from my own ops log over the 90-day window. I am keeping individual networks, countries and unit names private, but the shape of the result was:
North American interstitial units: AppLovin MAX at +18% eCPM, -2% fill rate, +12% ARPDAU vs AdMob — past the guardrails and decisively in MAX's favour.
Japanese native units: AppLovin MAX at +4% eCPM, -8% fill rate, -3% ARPDAU — disqualified by the fill-rate guardrail.
Monthly revenue across all six apps was running 4–6% higher on average. I read that mostly as the lift from running mediation at all, not as a single-network win.
After 90 days I decided not to do a full migration. Instead, I am gradually rebalancing the North American interstitial waterfall toward AppLovin MAX. The Antigravity sub-agent fleet keeps running, and I plan to have it emit the same kind of report every quarter.
Looking back at having anchored an indie business on AdMob since 2014, I think systems that let me observe with the same ruler over months are more durable than systems that produce a verdict. The real value of AI agents in monetisation work, at least from where I sit, is not the conclusion. It is the daily, consistent scaffolding that makes a real conclusion possible later. Thanks for reading.
Share
Thank You for Reading
Antigravity 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.