●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
Designing AdMob App Open Ad Frequency Without Hurting Retention — Operational Notes from Wallpaper Apps
Implementation notes from rebuilding the AdMob App Open Ad frequency design in a recent wallpaper app update. Minimum intervals, cold-start exceptions, and post-modal suppression are controlled dynamically through Remote Config, with Claude in Chrome handling the daily dashboard review.
import { ArticleImage } from "@/components/ArticleImage";
I am Masaki Hirokawa, an artist and indie developer. In early May 2026, the longest design discussion in my latest wallpaper app update was around the frequency design for AdMob App Open Ads. Unlike interstitials, these ads occupy the most sensitive moment in the session — the instant the user opened the app — and a careless setup will quietly chip away at D1 retention. Across over a decade of running iOS and Android apps as a personal developer since 2014, with a portfolio that has accumulated more than 50 million downloads, App Open Ads are the format I have seen split most sharply between "well-designed and profitable" versus "carelessly placed and a net loss".
Today I want to share the process of reworking the App Open Ad frequency design for two of my six wallpaper apps, building on top of code generated by Rork. The Claude in Chrome workflow I now use to check dashboards each morning made it natural to push the frequency parameters out to Firebase Remote Config so I can tune behavior remotely.
Why App Open Ads Behave Differently from Other Formats
AdMob App Open Ads occupy the full screen the moment the app launches or returns to the foreground. They visually resemble interstitials, but the timing and the user's mental state are very different.
Interstitials should appear between user-initiated actions — the user is aware they just navigated. App Open Ads appear right after the user opened the app on their own initiative, before they have done anything inside. The user did not open the app to look at ads, and ignoring that distinction is what causes silent retention loss.
In my own apps (such as Ukiyo-e Wallpapers), one variant lost 2.1 points of D1 retention in the week after we introduced App Open Ads. A sibling app with a more carefully designed frequency policy saw retention essentially flat while ARPDAU rose 18%. Same ad format, same scale, very different outcomes — the frequency design carries most of that delta.
Four Ground Rules I Operate By
After a few iterations, here are the four rules I currently apply to the wallpaper apps.
Never show on the first cold start: not after install, not after the first launch following an update. If you sour the first experience, no amount of subdued frequency later can win the user back.
Minimum 120 seconds between foreground returns: a tighter interval ramps up the "I see an ad every time I switch apps" feeling. Under 60 seconds in my data, churn visibly worsens.
Never show after a modal return: when the user returns from the photo library, share sheet, or purchase sheet, they switched away with clear intent and were always coming back. Hitting them with an ad on return punishes that intent.
Skip after rapid bouncing sessions: if the user closed the app within a second on consecutive sessions, skip the next foreground ad once. They might be searching for something quickly, not browsing.
AdMob's official documentation recommends each of these individually. In my experience, applying them in isolation does not produce the lift — the combination is what makes the system stable. The temple carpenters in both sides of my family taught me by example that careful assembly of small parts is what makes a structure last for decades. I find ad frequency design works the same way; tweaking one parameter rarely changes the picture.
✦
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
✦How to implement minimum intervals, cold-start exemptions, and post-modal exceptions so AdMob App Open Ads don't erode D1 retention while still capturing eCPM
✦Designing Firebase Remote Config conditions for per-cohort ad frequency, with a Claude in Chrome workflow for pulling daily churn metrics by A/B group
✦Three weeks of measured fill rate, eCPM, and D7 retention from a wallpaper app group with 50M+ cumulative downloads, including the surprising case where lowering frequency increased ARPDAU
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.
Rork can scaffold a basic App Open Ad loader when prompted, but to satisfy the rules above the generated code needs explicit state on top. I started with a minimal Expo + React Native hook in TypeScript.
// hooks/useAppOpenAd.tsimport { useEffect, useRef, useState } from "react";import { AppState, AppStateStatus } from "react-native";import { AppOpenAd, AdEventType, TestIds } from "react-native-google-mobile-ads";import { useRemoteConfig } from "./useRemoteConfig";import { useSession } from "./useSession";type AdState = "idle" | "loading" | "loaded" | "showing" | "error";export function useAppOpenAd() { const { minInterval, coldStartSkip } = useRemoteConfig(); const { lastShownAt, lastDismissedExternalAt, sessionStartedAt } = useSession(); const [state, setState] = useState<AdState>("idle"); const adRef = useRef<AppOpenAd | null>(null); // Initial load useEffect(() => { const ad = AppOpenAd.createForAdRequest( __DEV__ ? TestIds.APP_OPEN : "ca-app-pub-XXXXXXXX/YYYYYYYY", { requestNonPersonalizedAdsOnly: false, keywords: ["wallpaper", "lifestyle"] } ); adRef.current = ad; ad.addAdEventListener(AdEventType.LOADED, () => setState("loaded")); ad.addAdEventListener(AdEventType.ERROR, () => setState("error")); ad.load(); setState("loading"); }, []); // Decide to show on foreground useEffect(() => { const sub = AppState.addEventListener("change", (s: AppStateStatus) => { if (s !== "active") return; if (!adRef.current || state !== "loaded") return; // Rule 1: skip the first cold start if (coldStartSkip && Date.now() - sessionStartedAt < 3000) return; // Rule 2: minimum interval if (Date.now() - lastShownAt < minInterval * 1000) return; // Rule 3: skip after returning from external modal UI if (Date.now() - lastDismissedExternalAt < 5000) return; adRef.current.show(); setState("showing"); }); return () => sub.remove(); }, [state, minInterval, coldStartSkip, lastShownAt, lastDismissedExternalAt]); return { state };}
The point is to consolidate every time-based check inside this hook and to make minInterval and coldStartSkip arrive from Remote Config. From day one I want to be able to retune frequency in production without shipping a new binary.
Splitting Frequency by Cohort with Remote Config
Frequency design rarely lands at the right value on the first try. I use Firebase Remote Config to deliver per-cohort values and observe an A/B split. Here is the configuration I currently run for the Beautiful HD Wallpapers family.
On the Firebase Console side, I split conditions into "Cohort A (user-ID-suffix 0–4)" and "Cohort B (suffix 5–9)" and serve app_open_min_interval_sec as 60 for A and 180 for B. After three to five days, I cross-check the ad KPIs against D1 retention and converge toward the winning cohort's value.
A note on minimumFetchIntervalMillis: making it too short bumps into Remote Config rate limits. I keep it at one hour (3600 * 1000) by default. For emergency rollbacks I keep a separate path that does an explicit fetch followed by activate at launch.
Putting Modal Suppression on the Session Layer
The implementation that took the longest was the modal return suppression. The photo library, share sheet, in-app purchase sheet, camera, and OAuth web view are all routes where the user voluntarily leaves the app. Showing an App Open Ad when they return feels jarring because their intent was always to come back.
I chose to handle this on the session layer rather than the ad layer. The ad-side decision becomes trivially simple if it only needs one piece of session data: "when was the most recent external UI dismissed?"
import * as ImagePicker from "expo-image-picker";import { useSession } from "./hooks/useSession";export async function pickWallpaper() { const { markExternalShown } = useSession.getState(); markExternalShown(); return await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images });}
Once this structure was in place, adding new external modals later only required one line — markExternalShown() immediately before the call. Rork-generated screens that grow new modal flows do not regress the ad-side logic, since the responsibilities are cleanly separated.
Three Weeks of Measured Movement
Below are actual numbers from three weeks of running Cohort A (min_interval 60s) versus Cohort B (min_interval 180s). My naive expectation was that A would print more impressions and capture more revenue. The results were not what I expected.
Cohort A printed more impressions but lost on eCPM and retention. My read is that the ad networks detected the over-exposure through declining CTR and reduced their bids. After this three-week experiment, I converged on Cohort B's 180-second interval in production. The takeaway is counterintuitive on its face but consistent across the apps in my 50-million-download portfolio: lowering ad frequency can raise ARPDAU when the platform values longer-term engagement.
Delegating Daily Dashboard Review to Claude in Chrome
With this many parameters, manually opening AdMob and Firebase every morning accumulates real time cost. Alongside this update I switched daily trend review over to Claude in Chrome.
Concretely, I ask Claude in Chrome to do three things every morning.
Open the AdMob dashboard and pull per-app App Open Ad eCPM, fill rate, and CTR into a table
Cross-reference Firebase Console App Open Ad impression events with the previous day's values
Check whether today's app_open_min_interval_sec Remote Config value aligns with the CTR trend, and flag inconsistencies
The system only posts to Slack when CTR drops 0.3 points or more day over day. On normal days, no manual check happens. My role is to refine the prompt on days when an alert fires. After 12 years in the indie app business, one thing I have noticed is that watching ad KPIs daily dulls pattern recognition. Personally, the more days in a row I stare at AdMob, the worse my detection becomes. Delegating routine trend monitoring to AI and keeping my own attention for judgment calls fits my temperament better.
When I Accidentally Disabled the Cold-Start Skip
Soon after the rollout, one variant accidentally shipped with app_open_cold_start_skip = false. The cause was a misassigned condition in Remote Config — I had moved a variant into the wrong cohort. Within half a day, we lost roughly one star on store rating, and reviews showed up with "ad the moment I opened it" and "I get it, but I doubt I'll open it again." Then we looked at the Firebase value.
Two lessons stuck with me. First, Remote Config conditions need labels that any human can read at a glance. Naming a condition cohort_AB is the kind of abstraction that fools even its author later. Second, the risk of running an ad on cold start does not average out — for a user who opens the app once a day, the first launch's ad experience is the app's first impression. A small impression-count gain is easily wiped out by store rating damage.
My Current Priority Order
Putting the threads together, here is the priority order I think about when designing App Open Ad frequency.
Protect the very first cold start
Handle modal returns on the session layer
Keep the minimum interval inside a 90–180 second band, tuned through Remote Config
Track CTR, D1 retention, and ARPDAU together as three signals
Delegate dashboard reading to AI; keep human attention for judgment
The 50 million downloads that have supported my indie business were carried by ad revenue, which is why I refuse to be casual about wearing users down with ads. App Open Ads can be a kind format that captures real eCPM, or a quiet drag on retention, depending on how they are designed. The investment in careful design is, in my experience, more than repaid.
If you operate on the same format, I would love to hear how you have shaped your own Remote Config keys.
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.