●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
Monetizing a Rork App: Ad Pacing, Paywall Timing, and What Actually Moved Revenue
Adding ads and subscriptions to a Rork-generated app, rebuilt from the implementation up. Interstitial frequency control, when to show the paywall, and the review items that actually held up my submission — with working code.
I had shipped a small utility app generated with Rork, and the first weekend had just passed. When I opened the AdMob dashboard that Monday, the estimated revenue for the day read ¥38.
Installs had crossed 400. That is not a bad number. And still: ¥38.
My first instinct was to check the ad unit configuration. That was not the problem. The ads were serving correctly and filling normally. What I had never designed was where and how often they appeared.
Monetization turned out not to be an SDK task at all. It was a question of where in the experience the paid surface belongs. What follows is that design, taken all the way down to the implementation.
The revenue model is mostly decided by the shape of the app
Ads, subscriptions, or a one-time purchase — this choice feels like something you agonize over after building. In practice it is largely settled the moment you decide what kind of app you are making.
Two axes do most of the work: how many times a day someone opens the app, and whether value accumulates or evaporates after each use.
App shape
Model that fits
Why
Where it goes wrong
Games and puzzles
Ads (interstitial + rewarded)
Play has clear breakpoints, so there are natural seams for a full-screen ad
Firing on every breakpoint tanks ratings fast. Pacing is mandatory
Task managers and notes
Subscription
Data accumulates, switching cost rises, and a reason to stay develops
Too small a free tier and users leave before anything accumulates
Single-purpose calculators and converters
One-time purchase + ad removal
Sessions are short and infrequent, so recurring value is hard to justify
Nobody buys what nobody knows. A free tier has to feed it
Wellness and habit tracking
Subscription, annual-first
The goal horizon is long, which matches an annual commitment
Without visible weekly progress, month one is where they cancel
Wallpaper and asset delivery
Ads plus a one-time unlock
Each visit is brief, but visit frequency is high
Ads alone leave the per-user number flat
My own early mistake was trying to put a subscription on a single-purpose tool. No amount of added features creates a reason to pay monthly for something opened once a week. A mismatch between shape and model cannot be closed by better implementation.
What Rork gives you here is a cheap way to find out. Generation to store is short enough that backing out of a wrong model costs days rather than months.
✦
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 hook that governs interstitial frequency — cooldown, per-session cap, and a cold-start grace period
✦Entitlement checks that fail closed, plus a paywall trigger that fires exactly once instead of on every use
✦Which revenue model fits which app shape, and the pre-submission checklist built from real review rejections
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.
Before the monetization work, the shipping mechanics. Mistakes here delay revenue work by weeks.
1. Decide the paid surfaces before you write the prompt
Your Rork prompt is the spec. Naming the paid surfaces up front saves you from rearranging screens later.
Create an iOS and Android app called "Task Master".
Core features:
- Task list with add, delete, and complete
- Categories for organization
- Local storage only, no backend
- Offline first, dark mode support
Monetization surface (implement as placeholders):
- A settings row labeled "Upgrade" that opens an empty modal screen
- A banner slot pinned to the bottom of the task list screen
- An export action that is limited to 3 uses for free users
You do not need Rork to build the purchase screen itself. You only need the slot reserved. Adding it afterward means touching the navigation structure.
2. Test on device before you test on a simulator
The generated project comes down as an Expo app. Both the ads SDK and the purchases SDK are native modules, so simulators will not reproduce real behavior.
# Build a development client and install it on a real devicenpx expo prebuild --cleaneas build --profile development --platform ios
3. What you need on hand before submitting
Item
iOS
Android
Developer account
Apple Developer Program, billed annually
Google Play, one-time registration fee
Identifier
Bundle ID (e.g. com.yourname.taskmaster)
Package name, same format, permanent once published
Icon
1024×1024 px, no alpha channel
512×512 px
Screenshots
Required for the largest supported device size
At least two phone screenshots
Privacy policy
URL required, and it must cover your ad SDK
URL required, plus the Data Safety form
Data declarations
App Privacy, including advertising identifier use
Data Safety: what you collect and why
If ads are anywhere on your roadmap, declare them on the first submission. Amending privacy declarations after adding ads tends to lengthen that review cycle.
Without pacing, ads cost you your rating before they earn anything
Back to that ¥38. The cause was that I called the interstitial on every screen transition.
Impressions do go up. But when the same person sees full-screen ads repeatedly within a few minutes, they close the app — and they do not come back tomorrow. Impressions climb while retention falls. That combination is as far from revenue as you can get.
So I moved the decision of whether to show an ad into one place.
// hooks/useInterstitialGovernor.tsimport { useCallback, useEffect, useRef } from 'react';import AsyncStorage from '@react-native-async-storage/async-storage';import { InterstitialAd, AdEventType, TestIds,} from 'react-native-google-mobile-ads';const UNIT_ID = __DEV__ ? TestIds.INTERSTITIAL : 'YOUR_INTERSTITIAL_UNIT_ID';const LAST_SHOWN_KEY = 'ads:interstitial:lastShownAt';// Show only if: 4+ minutes since the last one, 3 per session max,// and never within the first 2 minutes after launchconst MIN_INTERVAL_MS = 4 * 60 * 1000;const MAX_PER_SESSION = 3;const COLD_START_GRACE_MS = 2 * 60 * 1000;export function useInterstitialGovernor() { const ad = useRef(InterstitialAd.createForAdRequest(UNIT_ID)).current; const loaded = useRef(false); const shownInSession = useRef(0); const sessionStartedAt = useRef(Date.now()); useEffect(() => { const offLoaded = ad.addAdEventListener(AdEventType.LOADED, () => { loaded.current = true; }); const offClosed = ad.addAdEventListener(AdEventType.CLOSED, () => { loaded.current = false; ad.load(); // preload for the next opportunity }); ad.load(); return () => { offLoaded(); offClosed(); }; }, [ad]); const maybeShow = useCallback(async () => { if (!loaded.current) return false; if (Date.now() - sessionStartedAt.current < COLD_START_GRACE_MS) return false; if (shownInSession.current >= MAX_PER_SESSION) return false; const raw = await AsyncStorage.getItem(LAST_SHOWN_KEY); const lastShownAt = raw ? Number(raw) : 0; if (Date.now() - lastShownAt < MIN_INTERVAL_MS) return false; await ad.show(); shownInSession.current += 1; await AsyncStorage.setItem(LAST_SHOWN_KEY, String(Date.now())); return true; }, [ad]); return { maybeShow };}
Call sites simply ask, and the hook decides.
const { maybeShow } = useInterstitialGovernor();const handleTaskComplete = async (taskId: string) => { await completeTask(taskId); // The hook owns the decision; the screen carries no conditions await maybeShow();};
I structured it this way because scattering the conditions across screens always falls apart. Every new screen adds another "show here, skip there" judgement until nobody can describe the overall frequency. With the logic in one file, retuning pacing means changing three constants.
The two-minute grace period exists for a specific reason: someone who sees an ad seconds after first launch has not experienced any value yet. Churn at that moment lands directly in your reviews.
The four-minute cooldown came from my own median session length of roughly five minutes. That is a number you should read off your own analytics — borrowed constants stop meaning anything.
Design the subscription backward from why nobody cancels
Once pacing was settled, I moved to subscriptions. The first decision was not price. It was which way the entitlement check should fail.
Fetching purchase state fails routinely — network drops, receipt validation lags. If "unknown" means "treat as paid," free users get paid features. Fail the other way.
// lib/entitlement.tsimport Purchases, { CustomerInfo } from 'react-native-purchases';const ENTITLEMENT_ID = 'pro';export async function configurePurchases(apiKey: string, appUserId?: string) { await Purchases.configure({ apiKey, appUserID: appUserId ?? null });}export function isPro(info: CustomerInfo | null): boolean { // Unknown state means free. Default to not granting access. if (!info) return false; return info.entitlements.active[ENTITLEMENT_ID] !== undefined;}export async function refreshEntitlement(): Promise<boolean> { try { const info = await Purchases.getCustomerInfo(); return isPro(info); } catch { return false; }}
Then the timing. I removed the launch-time paywall and narrowed it to the single moment a free user hits the limit.
// lib/paywall.tsimport { refreshEntitlement } from './entitlement';const FREE_EXPORT_LIMIT = 3;export async function shouldOpenPaywall(exportCount: number): Promise<boolean> { // Never to paying users if (await refreshEntitlement()) return false; // Never before they have felt the value if (exportCount < FREE_EXPORT_LIMIT) return false; // Exactly once, at the limit. After that, the settings row carries it. return exportCount === FREE_EXPORT_LIMIT;}
The equality check is the whole point. Using >= means the paywall reappears every single time a user past the limit touches the feature. That is not persuasion — it is obstruction.
Three free uses came from testing: two was not enough for the value to register, five diluted the reason to pay. Where your own limit sits depends on whether one use completes the core job. I went deeper on limits and paywall placement in When Free Rork Users Become Paying Subscribers.
On trials: I attach a three-day trial to the annual plan only. Attached to monthly, a large share cancelled the moment the trial ended. On annual, the trial actually asks a useful question — is this worth a year?
Review items that held me up, and the checklist they produced
Adding monetization widens the surface reviewers examine. These are the flags I actually received.
Flagged area
What was said
Fix
Missing restore
No restore-purchases control on the purchase screen
Added "Restore purchases" to both the paywall and settings
Price disclosure
Renewal period and price not shown near the purchase button
Render the store-provided localized price and period under the button
Advertising identifier
App Privacy did not declare advertising identifier use
Updated the declaration and the privacy policy text
Ad overlap
On smaller screens the banner covered an action button
Reserved safe-area padding at the end of the list
Review credentials
No way to test screens behind sign-in
Put a test account and repro steps in the review notes
Restore and price disclosure are not in Rork's generated output. I now treat both as required parts of any purchase screen I assemble.
Review notes get dismissed as busywork, but three lines of reproduction steps reliably removes one round trip. I keep a template and only edit the feature diff each time.
Reading the numbers: a model, and what actually moved them
What follows is a model, not a promise. Real numbers swing hard with app shape and traffic source. Treat it as a skeleton for reasoning.
For an ad-supported app, monthly revenue is roughly:
monthly ≒ DAU × impressions per user per day × eCPM ÷ 1000 × 30
At 3,000 DAU, 2 impressions per user per day, and an eCPM of ¥400, that lands near ¥72,000 a month. Three variables are available.
Variable
How to move it
Side effect
DAU
ASO and a steady update cadence to lift retention
Slow. Months before it shows
Impressions per user
Add ad placements
Past a point, retention falls and DAU with it
eCPM
Revisit ad formats and demand configuration
Misconfigured, it moves the wrong way
Adding placements is the fast lever, but it eats DAU, so the product of the two often shrinks over a quarter. Noticing that the first and second terms pull against each other is what pushed me toward pacing.
Moving conversion from 1% to 2% doubles revenue. Repositioning a paywall gets you there faster than doubling installs. That asymmetry is why I spend my time on the implementation side.
Three changes that measurably helped:
Rebuilding the first screenshot. I replaced the feature grid with the screen you see after finishing a task. In search results, effectively only the first image gets looked at.
Moving the review prompt. Off first launch, onto the moment right after the third completed core action. Same wording, different average rating.
Keeping update intervals even. Biweekly for the first three months, then monthly. Consistency of interval mattered more for ratings than the size of each release.
Four places this usually goes sideways
Adding placements and losing retention. Chasing impressions alone leads here every time. The number worth watching is impressions paired with next-day retention, never impressions alone.
Ignoring launch-week defects. Low ratings from the first days linger in the average for a long while. Reserve the first 72 hours after release for crash monitoring and nothing else.
Shipping and stopping. With no announcement anywhere, store search becomes your only acquisition channel. Simply listing three places to post, a week before launch, changes the first-week numbers.
Drifting away from feedback. Reviews and support mail are the only signal about what to build next. Replying within a week often gets a low rating revised.
Where to start
If your app is already live and revenue is below what you expected, put impressions and next-day retention side by side first. If only impressions are climbing, drop in the pacing hook above and tune the constants to your own session length.
If you have not shipped yet, reserve the paid surfaces as placeholders now. It is far cheaper than retrofitting them.
Numbers take time to move. I am still adjusting mine. Thank you for reading this far.
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.