●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking●RORKMAX — Rork Max generates pure Swift instead of React Native, enabling true native apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●APPLE — Rork's 2026 direction has a clear theme of native empowerment across the Apple ecosystem●EXPO — Standard builds run on React Native and Expo, so you're left with a real project structure and code you can keep working on●FUNDING — Rork recently raised $15M and now sees over 743,000 monthly visits with 85% growth●PRICING — Rork is free to start, with paid plans from $25/month and Rork Max at $200/month●CROSS — Rork builds iOS, Android, and web from a single prompt, finished off with a bit of follow-up tweaking
How I Layered Rewarded, Interstitial, and App Open Ads to Lift ARPDAU 1.4× on My Wallpaper App
Optimizing AdMob's three formats individually hit a ceiling — they were eating each other's impressions. After redesigning Rewarded, Interstitial, and App Open as a coordinated 3-layer system with strict role separation and an exclusion gate, ARPDAU climbed 1.4× over nine weeks. The full playbook, code, and weekly numbers.
My wallpaper app's AdMob revenue had been flat for about six months. I'm Masaki Hirokawa (@dolice), an artist and indie iOS/Android developer. I've been shipping apps since 2014, and after my catalog crossed 50 million cumulative downloads, I started running into a wall: optimizing each ad format individually no longer moved the needle. When I pushed Rewarded harder, Interstitial impressions dropped. When I added App Open, Rewarded completion rates fell. The three ad formats were cannibalizing each other.
Over nine weeks this spring, I rebuilt the AdMob stack around a single idea: treat the three formats as a coordinated 3-layer system, not three independent levers. ARPDAU (average revenue per daily active user) climbed 1.4× and monthly AdMob revenue returned to a number I had only seen once before — over one million yen in a single month. Here is the design and the operational log.
Why You Have to Think in Three Layers
The official AdMob docs cover Rewarded, Interstitial, and App Open separately. Read in isolation, each guide reasonably tells you to maximize frequency without hurting UX. The problem starts the moment you put all three into the same app. Their guidelines start interfering with each other.
The first collision I hit looked like this: Rewarded promised "watch an ad to unlock three extra wallpapers," but App Open fired immediately before the unlock prompt. Users perceived a wall of ads and bounced. Crashlytics reported zero crashes, yet Rewarded impressions dropped about 30% that week. The root cause was simple — three formats were competing for a single finite resource: the user's attention.
The essence of 3-layer thinking is to assign each format a clear role — lead actor, seasoning, and leak bucket — and have them defer to one another at runtime. I only put that into words after running a wallpaper catalog at 50M+ downloads long enough to feel that single-format optimization had hit a ceiling that no amount of fine-tuning could break through.
The Three Roles — Lead Actor, Seasoning, Leak Bucket
Here is the split I locked in over nine weeks.
The lead actor is Rewarded. It is the only format where the user makes an active choice — "watch this ad and I get something." That positive exchange shows up in higher eCPM (2–3× the others in my apps), higher retention for users who complete a view, and lower review-page complaints. I now start every monetization design by maximizing where this format can fire.
The seasoning is Interstitial. It fits at session boundaries — moments where the user is between actions, not in the middle of one. On a wallpaper app, that means "after swiping through ten wallpapers, when switching categories." The opposite — firing while the user is actively choosing something — pushes CTR up but cuts retention. I tested that once in early 2026 and Day 7 retention fell 11 points. I haven't forgotten.
The leak bucket is App Open. It earns its keep by catching sessions where neither of the other two formats fired. If Rewarded never triggered and Interstitial got rate-limited, App Open recovers some revenue. Promote it to a starring role and users get an ad on every cold launch — a fast track to one-star reviews.
✦
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
✦Role split (lead actor / seasoning / leak bucket) for Rewarded, Interstitial, and App Open — plus the exclusion-gate code that eliminated ad-on-ad collisions
✦The four ARPDAU metrics I track weekly and the Firebase Remote Config-driven cap tuning that moved the needle
✦Three concrete failure modes from the nine-week rollout, including the moment a 'permanent unlock' reward gutted next-week views by 70%
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.
Once Rewarded is the star, you invest in moments where users want to watch. Three things worked on my wallpaper app.
First: turn visibility into the currency. Show the high-resolution version of a wallpaper at low resolution to free users, then unlock the high-res for 48 hours after a Rewarded view. The trick is to render the low-res hint clearly. If the user can't already see what they would get, the desire never forms.
Second: stage the UI around completed views. Three views unlocks a sort option, ten unlocks an extra theme palette. Store the view count locally and tier the rewards. Wire the thresholds through Firebase Remote Config so you can A/B test them.
Third: cap daily Rewarded views to give them scarcity. Capping at 10 per day flipped my users into a "let me use today's views before they reset" mindset — the same psychology as a restaurant's daily special. Completion rate jumped 18%.
// Rewarded daily quota (React Native + AsyncStorage)const REWARDED_DAILY_CAP = 10;async function canShowRewarded(): Promise<{ allowed: boolean; remaining: number }> { const today = new Date().toISOString().slice(0, 10); const raw = await AsyncStorage.getItem(`rewarded_count_${today}`); const count = raw ? parseInt(raw, 10) : 0; return { allowed: count < REWARDED_DAILY_CAP, remaining: Math.max(0, REWARDED_DAILY_CAP - count), };}async function recordRewardedView(): Promise<void> { const today = new Date().toISOString().slice(0, 10); const key = `rewarded_count_${today}`; const raw = await AsyncStorage.getItem(key); const count = raw ? parseInt(raw, 10) : 0; await AsyncStorage.setItem(key, String(count + 1));}
The hidden value here is the "remaining quota" UI. Showing "7 ad views left today" measurably lifts the rate at which users actually start a view.
Move Interstitial Caps from "Session" to "Action" Granularity
AdMob's console has frequency caps — "max 2 per session," "minimum 60 seconds between displays." That wasn't enough.
The reason I switched from session-level caps to action-level caps was this failure mode: a user who only browsed wallpapers got an Interstitial the moment they tapped Settings. From their perspective they had just opened a settings screen. From mine, an ad interrupted them. Bounce was the rational response.
Action-granular caps count meaningful user actions inside the app and only allow an Interstitial after a threshold. Meaningful = swipes through wallpapers, category selections, favorites added, shares — the things a user actively chose to do.
// Action-granular Interstitial caplet meaningfulActionsThisSession = 0;let lastInterstitialAt: number | null = null;const ACTION_THRESHOLD = 8; // 8 actions to unlock one ad slotconst MIN_INTERVAL_MS = 90_000; // at least 90s since the last showfunction recordAction(actionType: 'swipe' | 'category' | 'favorite' | 'share'): void { meaningfulActionsThisSession += 1;}function shouldShowInterstitial(): boolean { if (meaningfulActionsThisSession < ACTION_THRESHOLD) return false; const now = Date.now(); if (lastInterstitialAt && now - lastInterstitialAt < MIN_INTERVAL_MS) return false; return true;}function notifyInterstitialShown(): void { meaningfulActionsThisSession = 0; lastInterstitialAt = Date.now();}
Interstitial impressions per session dropped from 3 to 1.8, but CTR rose from 0.42% to 0.71%. Total Interstitial revenue stayed flat, and bounce dropped sharply. In retrospect this echoes Google's Helpful Content logic — prioritize fit over volume and the revenue lasts longer.
App Open Is a Leak Bucket, Not a Headline Act
If App Open is your headline act, every launch shows an ad. That generates a class of "exit churn" that Crashlytics never sees. I now treat App Open as a recovery slot for sessions where the other two formats didn't fire.
Three rules. First: do not fire on cold launch by default. Skip if either Rewarded or Interstitial fired within the first 30 seconds of the session. Second: skip on returns within five minutes of the last session ending — that's "the user briefly opened another app and came back." Third: cap at three views per day. Same philosophy as the action-granular cap on Interstitial — protect UX first.
// App Open eligibility (leak-bucket rules)const APP_OPEN_DAILY_CAP = 3;const RETURN_GRACE_MS = 5 * 60 * 1000;async function shouldShowAppOpen( sessionStartMs: number, rewardedShownAt: number | null, interstitialShownAt: number | null, lastSessionEndMs: number | null,): Promise<boolean> { const now = Date.now(); // Rule 1: skip if another format fired in the first 30s if (rewardedShownAt && rewardedShownAt - sessionStartMs < 30_000) return false; if (interstitialShownAt && interstitialShownAt - sessionStartMs < 30_000) return false; // Rule 2: skip on quick returns if (lastSessionEndMs && now - lastSessionEndMs < RETURN_GRACE_MS) return false; // Rule 3: daily cap const today = new Date().toISOString().slice(0, 10); const raw = await AsyncStorage.getItem(`app_open_count_${today}`); const count = raw ? parseInt(raw, 10) : 0; if (count >= APP_OPEN_DAILY_CAP) return false; return true;}
After the switch, App Open CTR doubled from 0.18% to 0.34%. The remaining impressions only land at moments where users haven't yet felt fatigued.
Exclusion Gate — Stop the Three Formats from Firing Together
Even with role splits and per-format caps in place, nothing in the code stops two formats from firing on the same event. Add a wallpaper to favorites? Rewarded's daily quota might be open, the action threshold for Interstitial might be met, and both fire back to back.
The fix is an exclusion layer I call AdGate. No screen calls the ad SDK directly. Every ad request goes through the gate, and the gate picks at most one format for the current event.
// AdGate exclusion layertype AdEvent = 'category_change' | 'favorite_add' | 'app_resume' | 'session_milestone';async function decideAdToShow(event: AdEvent): Promise<'rewarded' | 'interstitial' | 'appOpen' | 'none'> { // Priority 1: Rewarded if the user explicitly asked for it at a session milestone if (event === 'session_milestone' && await canShowRewardedOnMilestone()) { return 'rewarded'; } // Priority 2: Interstitial if the action threshold has been crossed if (shouldShowInterstitial()) { return 'interstitial'; } // Priority 3: App Open as the leak bucket if (event === 'app_resume' && await shouldShowAppOpen(/* ... */)) { return 'appOpen'; } return 'none';}
The day I shipped the gate, "two ads in a row" reports disappeared. "Too many ads" review comments dropped from about 15 per month to 3 over nine weeks.
Four Metrics I Watch Weekly for ARPDAU
ARPDAU is not just "show more ads." Four dashboards live on my weekly review.
Format-level ARPDAU breakdown. Rewarded, Interstitial, and App Open each contribute a number per DAU. A single combined ARPDAU figure hides which lever is moving.
Retention × ARPDAU cohort matrix. Day 1, Day 7, and Day 30 retention crossed with ARPDAU for each cohort. If Day 30 ARPDAU is at least 2× Day 1, your long-tail revenue design is working.
Per-format CTR and completion rate. Rewarded completion rate, Interstitial skip rate (closed mid-roll), App Open instant-close rate. Anything trending worse is a candidate for a frequency cut.
"No-ad session share." With the gate and caps in place, some sessions never see an ad at all. Above 30% means you're leaving money on the table. Below 5% means you're over-serving. I aim for 15–20%.
I pipe these from Firebase BigQuery Export and the AdMob Reporting API into a morning Slack post. I also have Claude in Chrome pull yesterday's deltas and propose three hypotheses for any anomaly. That part is a separate article, but it keeps the operational overhead inside what one person can sustain.
Nine-Week Timeline — The Numbers to 1.4×
For reference, here are the weekly numbers from one wallpaper app (DAU ~12,000).
Week 0 baseline: ARPDAU $0.038. Format split — Rewarded $0.012, Interstitial $0.018, App Open $0.008.
Weeks 1–2 (Rewarded tweaks): Rewarded ARPDAU doubled from $0.012 to $0.024. Completion rate climbed from 42% to 57%. Interstitial impressions dipped, dropping that format's ARPDAU from $0.018 to $0.014. Combined: $0.046.
Weeks 3–5 (action-granular Interstitial cap): impressions per session down, CTR up. Interstitial ARPDAU recovered to $0.020. Mid-session bounce fell from 23% to 17%.
Weeks 6–7 (App Open leak-bucket rules): App Open ARPDAU $0.008 → $0.011. Combined ARPDAU: $0.055.
Weeks 8–9 (AdGate exclusion layer): collisions eliminated. "Too many ads" complaints crashed. Average rating climbed from 3.8 to 4.1. Final ARPDAU was $0.053 — slightly under the peak week, but Day 30 retention rose enough that DAU × ARPDAU (monthly total revenue) reached 1.4× the baseline.
I optimize for the monthly total — DAU × ARPDAU — not the peak weekly ARPDAU number alone.
Three Failure Modes to Avoid
Three things I broke during the rollout, in case they save anyone time.
Make rewards too strong and they evaporate. One week I tried a "permanent unlock" instead of 48 hours. Rewarded views jumped 5× that week — and crashed to 30% of baseline the following week. Once everything is permanently unlocked, there is no reason to watch another ad. Design rewards that can be re-consumed.
Don't pick a single action threshold by feel. I hardcoded "8 actions per Interstitial" at first. Some user segments found 8 too short; others found it way too long. Run Remote Config A/B tests and tune the threshold per user segment to find the value that maximizes retention.
Build the AdGate from day one. Adding it later means hunting every direct ad-SDK call across screens. It took me three days to migrate everything. For any new app I start, AdGate is now a non-negotiable starting layer.
That's the nine-week log. The 3-layer framing is not in the official AdMob documentation as far as I have been able to find — single-format optimization is well covered, but no resource I located addressed format-to-format coordination. After 12 years of indie development, the mediation between formats is what raises the revenue ceiling another level.
If you run a wallpaper app or an entertainment app on ads, I hope some of this is useful.
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.