●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) are now in the model menu●RN 0.88 — The stable release was still unpublished on its Oct 12 target. rc.4 (Oct 7) is the newest; 0.87.1 is Latest●Q&A — Readers report expo-widgets showing a black widget in production builds only●WINDOWS — A walkthrough shows how to ship an iPhone app from Windows only, with no Mac●NEW — Three checks to run first when notifications stop arriving in Expo Go●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) are now in the model menu●RN 0.88 — The stable release was still unpublished on its Oct 12 target. rc.4 (Oct 7) is the newest; 0.87.1 is Latest●Q&A — Readers report expo-widgets showing a black widget in production builds only●WINDOWS — A walkthrough shows how to ship an iPhone app from Windows only, with no Mac●NEW — Three checks to run first when notifications stop arriving in Expo Go
Mobile App Monetization Foundations With Rork — Choosing Between Ads, IAP, and Subscriptions
A monetization primer for Rork-built mobile apps: ads, in-app purchases, and subscriptions compared by fit and implementation pitfalls, plus a small estimator that turns your own DAU, conversion, and churn assumptions into side-by-side monthly revenue, AdMob mediation practice, and cohort retention code.
The week after a release, I opened Sales and Trends in App Store Connect first thing in the morning and looked at a column of zeros. Downloads were coming in every day. The first ratings had appeared. Only the money column refused to move — and I've come to think that this screen is where every monetization question begins.
Having run wallpaper apps and ambient-sound apps as a solo developer for a long time, the one thing that hasn't changed is this: how you build and how you earn are two different problems. Rork has made the first one shorter than I ever expected. It hasn't shortened the second one by a single day.
What I'd like to say up front is that what follows isn't someone else's revenue. It's a set of rulers you can hold against your own app. I'll go through ads, in-app purchases, and subscriptions from three angles — what each fits, where the implementation bites, and how to estimate them with your own assumptions.
The Three Models, Understood by Fit
The first is the ad model. You show ads served by networks such as Google AdMob, Meta Audience Network, or Unity Ads, and earn on impressions and clicks. Revenue per user per month is small; in what I've observed, casual apps aimed at Japan land somewhere between a few yen and a few dozen yen.
This fits apps people expect to be free: wallpapers, weather, timers, lifestyle utilities. Most of the apps I've run for years sit in this bucket.
The second is in-app purchases (IAP). You sell something inside the app — remove ads, unlock a feature, add content. Revenue per paying user is far higher than ads, but the share of users who ever pay usually stays in the low single digits.
This fits apps with a differentiator you can state in one sentence: "this feature is worth paying for." Photo editors, PDF tools, professional utilities, and add-on content in games live here.
The third is subscriptions. Users pay monthly or yearly, revenue per user tends to be the highest of the three, and it stabilizes over time. The precondition is that you keep delivering value, and managing churn becomes the center of the operation.
This fits apps whose value accumulates with use: learning, health tracking, budgeting, content services. Apps with AI built in tend to suit subscriptions by nature, I find.
The Decision Framework
"Just add everything" is the first trap. An app with ads, IAP, and subscriptions all at once ends up with a cluttered UI, users can't tell what they're supposed to pay for, and in the end they pay for nothing. I did this in an early app of mine and every one of the three ended with a half-hearted number.
I use four axes when I pick a model.
The first is expected DAU. If many people will open the app daily, ads are the first candidate; if you'll stay in the low thousands, IAP or subscriptions become necessary. This comes from working backward from revenue per user, and once you drop your own numbers into the estimator below, the gap becomes obvious.
The second is how strong the differentiator is. If you can clearly say "Pro unlocks this," go IAP or subscription. If the differentiation is weak, ads and volume are the game.
The third is frequency of use. A daily-use app can sustain a subscription. An app opened less than once a week turns a subscription into a monthly reminder to cancel.
The fourth is the audience's willingness to pay. Younger and casual audiences lean toward ads; professionals and business users tend to accept subscriptions or higher-priced IAP.
In my own portfolio, weakly differentiated apps like wallpapers are ad-centered, feature-led apps like photo editing are IAP-centered, and tools people open daily for work are subscription-centered. Before adding a model, make one model work. That sentence is where my decisions start now.
✦
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
✦An estimator that takes DAU, eCPM, conversion, and churn and lines up monthly revenue for all three models, so you decide from your own numbers
✦The mediation stack that lifted rewarded eCPM about 1.4x, and where waterfall still beats bidding
✦Cohort retention code, and why trial conversion and post-first-month retention must be read separately
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.
Estimate With Your Own Numbers — One Ruler for Three Models
If there's one thing to take away from this article, it's this section. Looking at someone else's revenue tells you nothing about your app, but putting your own assumptions into the same shape of formula and lining up the three models shows you, tangibly, which lever actually moves revenue.
// The smallest ruler that estimates monthly revenue for all three models from comparable inputstype AdsInput = { dau: number; impressionsPerDau: number; ecpmJpy: number };type IapInput = { mau: number; payerRate: number; arppuJpy: number };type SubInput = { trialStartsPerMonth: number; trialToPaid: number; // trial-to-paid conversion (0–1) monthlyPriceJpy: number; monthlyChurn: number; // monthly churn of paying users (0–1)};export function estimateAds({ dau, impressionsPerDau, ecpmJpy }: AdsInput) { // eCPM is revenue per 1,000 impressions, hence the division return Math.round((dau * impressionsPerDau * 30 * ecpmJpy) / 1000);}export function estimateIap({ mau, payerRate, arppuJpy }: IapInput) { return Math.round(mau * payerRate * arppuJpy);}export function estimateSubSteadyState({ trialStartsPerMonth, trialToPaid, monthlyPriceJpy, monthlyChurn,}: SubInput) { // Steady-state payers when the same number of trials arrives every month = new payers ÷ monthly churn const newPayers = trialStartsPerMonth * trialToPaid; const steadyPayers = monthlyChurn > 0 ? newPayers / monthlyChurn : Infinity; return { steadyPayers: Math.round(steadyPayers), monthlyRevenueJpy: Math.round(steadyPayers * monthlyPriceJpy), // Rough months until you approach steady state; the inverse of churn is the time axis monthsToSettle: monthlyChurn > 0 ? Math.round(1 / monthlyChurn) : Infinity, };}
Here it is with assumptions plugged in. Every number below is one I chose for illustration, and the point is to replace them with your own.
Model
Assumptions used
Estimated monthly revenue
The lever that matters most
Ads
5,000 DAU · 4 impressions per user · ¥150 eCPM
about ¥90,000
DAU (more impressions erode UX, so that lever has a ceiling)
IAP
10,000 MAU · 2% payer rate · ¥800 ARPPU
about ¥160,000
Payer rate (one point of movement changes revenue by half)
about ¥360,000 at steady state (roughly 7 months to get there)
Churn (halve it and steady-state revenue doubles)
What I noticed when I built this table is that only the subscription estimate contains time. Ads and IAP pay this month for this month's numbers; a subscription doesn't reach steady state until you've waited roughly the inverse of your churn rate. You can read from this how premature it is to give up on a subscription after the first month's sales.
The ad row tells the opposite story: adding impressions without growing DAU runs into a ceiling quickly. I once drifted toward showing more ads per user and watched my ratings slip. After staring at this formula again, I settled on a rule — if I'm growing something, it's DAU; if I'm cutting something, it's impressions.
Implementing the Ad Model
For Rork apps, AdMob is the easiest network to wire up, and its reporting is detailed. Here's what I keep in mind.
// AdMob banner in React Native + Rorkimport { View } from "react-native";import { BannerAd, BannerAdSize, TestIds } from "react-native-google-mobile-ads";function AppScreen() { // Use test IDs in development. Tapping production ads on your own device gets flagged as invalid traffic const adUnitId = __DEV__ ? TestIds.BANNER : "ca-app-pub-XXXXXXXXXXXXXXXX/YYYYYYYYYY"; return ( <View style={{ flex: 1 }}> <YourMainContent /> <BannerAd unitId={adUnitId} size={BannerAdSize.ANCHORED_ADAPTIVE_BANNER} requestOptions={{ requestNonPersonalizedAdsOnly: false, }} /> </View> );}
Three points bite in practice.
First, choose formats deliberately. Banners sit at the bottom permanently, interstitials take over the screen between transitions, and rewarded ads are watched by choice in exchange for something. In my apps, "watch a rewarded ad to unlock a feature temporarily" has been the best compromise between revenue and experience.
Second, hold ads back for a while after first launch. Showing one immediately damages the experience and goes straight to your star rating. I leave a grace period of at least 30 seconds to a minute.
Third, throttle interstitials. Show one on every transition and users leave without a word. Rules like "every third time" or "at least five minutes since the last one" work well, and I keep them in a single place that every ad call goes through.
Lifting eCPM With AdMob Mediation
Once you've chosen ads, what decides revenue is where the ads come from. AdMob alone works, but mediation — bundling several networks — changes eCPM (revenue per 1,000 impressions) at the same impression count.
For a long time I've kept AdMob as the mediation hub with Meta Audience Network, Unity Ads, AppLovin, and Pangle underneath. When I moved a wallpaper app from AdMob-only to mediation, rewarded eCPM rose roughly 1.4x. The reason is simple: when several networks compete for the same impression, the one bidding highest wins it.
The judgment call is waterfall versus bidding. Waterfall assigns each network a fixed priority and price floor and asks them in order; bidding lets every network bid at once and takes the top price. What worked for me was leaning main placements toward bidding to cut missed fills while keeping a few predictable secondary networks in the waterfall. A full waterfall is easier to configure, but request latency stacks up and the wait before an ad appears grows.
The other thing that sharpens your read on revenue is knowing how eCPM swings. In what I've observed, rewarded eCPM tends to run higher when North America is awake than when Japan is asleep, and the country mix alone moves monthly revenue by about a fifth for the same app. That's why I look at reports by country × format × time of day rather than at the total — it shows which placements deserve more weight.
Format
eCPM range I've observed (Japan)
UX cost
When to add weight
Anchored banner
Low (tens of yen)
Small
A thin base that's always there
Interstitial
Medium
Medium
Only at natural breaks
Rewarded
High (hundreds of yen)
Small (opt-in)
The main earner, paired with feature unlocks
These move with genre and season, so please treat them as a way to grasp the ordering between formats rather than as absolute values. I initially tried to add more of the high-eCPM rewarded placements and nearly broke the experience, then narrowed down to the placements users don't resent because they opt in. The numbers told me not just where to grow but where to stop. Network-by-network configuration lives in a separate piece, AdMob Monetization Strategy for Rork Apps.
Implementing IAP
IAP in React Native means react-native-iap, product setup in both stores, and server-side verification. Rork makes the screen side easy; the one line you still have to draw yourself is where a purchase is declared "finished."
import { useEffect } from "react";import { Platform, View, Text, TouchableOpacity } from "react-native";import { useIAP } from "react-native-iap";const productIds = ["pro_unlock", "premium_pack", "feature_x"];function PurchaseScreen() { const { connected, products, getProducts, requestPurchase, currentPurchase, finishTransaction } = useIAP(); useEffect(() => { if (connected) getProducts({ skus: productIds }); }, [connected]); // Purchase results arrive as changes to currentPurchase, not as a callback useEffect(() => { if (!currentPurchase) return; (async () => { // 1. Send to your server and verify first (never trust the client-side result) const ok = await verifyOnServer(currentPurchase); // 2. Only then finishTransaction. Doing this first consumes the purchase before verification if (ok) await finishTransaction({ purchase: currentPurchase, isConsumable: false }); })(); }, [currentPurchase]); const handlePurchase = async (sku: string) => { try { // v12 takes different argument shapes on iOS and Android. Check the type definitions of the version you installed await requestPurchase(Platform.OS === "ios" ? { sku } : { skus: [sku] }); } catch (err) { console.error("Purchase failed", err); } }; return ( <View> {products.map((p) => ( <TouchableOpacity key={p.productId} onPress={() => handlePurchase(p.productId)}> <Text>{p.title} - {p.localizedPrice}</Text> </TouchableOpacity> ))} </View> );}
Three points again.
First, verify on the server, always. Trusting the client alone means accepting a tampered result and unlocking features for free. Apple's legacy verifyReceipt is deprecated; the current path is the App Store Server API, which returns signed transaction info from a transaction ID. On Google, the Play Developer API's purchases.products.get (one-time) and purchases.subscriptionsv2.get (subscriptions) do the job. Don't call finishTransaction until verification passes — that ordering is the one thing I never let slip.
Second, provide Restore Purchases. Without it, someone who switches phones writes the review I dread most: "I paid and it's gone." Apple's review team flags this often.
Third, revisit pricing by country. ¥480 in Japan is not automatically equivalent to $4.99 in the US. Looking through the store price sheets and moving toward each market's purchasing power visibly changed the distribution of purchases in my apps. I can't promise a multiplier, but there's no reason to leave the default flat conversion in place either.
Implementing Subscriptions
Subscriptions are a special case of IAP, but the operational complexity is a different animal. Here are the three things that weigh most in the subscription apps I run.
First, trial length. Three, seven, or fourteen days changes conversion. For me, tools that need time to sink in settled at 14 days and content apps at 7. Three days tended to expire before people felt the value, and they canceled before converting.
import { Platform } from "react-native";import { requestSubscription } from "react-native-iap";const subscriptionId = "monthly_pro";// Android requires an offerToken per offer; take it from the getSubscriptions() resultconst handleSubscribe = async (offerToken?: string) => { try { await requestSubscription({ sku: subscriptionId, ...(Platform.OS === "android" && offerToken ? { subscriptionOffers: [{ sku: subscriptionId, offerToken }] } : {}), // The trial lives in the store product settings. In code, keep the rule: finishTransaction only after verification andDangerouslyFinishTransactionAutomaticallyIOS: false, }); } catch (err) { console.error("Subscription failed", err); }};
Second, state management. Someone who cancels must keep Pro until the current period ends. Record expiresAt on the server and gate features solely on expiresAt > now — that's the arrangement with the fewest accidents in my experience.
Third, information right before cancellation. Show, once, which features go away and that access continues through the period. In my apps, the month I added that one screen, the number of people who stayed became visibly larger. Not by scaring anyone — simply by stating accurately what they'd lose.
Managing Subscription Churn With Numbers — Retention by Cohort
With subscriptions, revenue is decided less at the door by acquisition than at the exit by churn. The tool that helps here is the cohort view: group users by the month they signed up and follow each group's retention afterward.
In the work-focused app I run, trial-to-paid conversion sits around 30%, and next-month retention among those who clear the first month stays just under 90%. What matters is reading those two numbers separately. Low conversion points to the trial experience or the price; low retention after the first month points to a hole in the value itself. Lumping them into "churn is high" gives you nothing to act on.
// Minimal example: retention N months out, by signup-month cohorttype Sub = { userId: string; startedAt: Date; canceledAt: Date | null };function retentionByCohort(subs: Sub[], monthsOut: number) { const cohorts: Record<string, { total: number; retained: number }> = {}; for (const s of subs) { const key = `${s.startedAt.getFullYear()}-${s.startedAt.getMonth() + 1}`; cohorts[key] ??= { total: 0, retained: 0 }; cohorts[key].total++; const boundary = new Date(s.startedAt); boundary.setMonth(boundary.getMonth() + monthsOut); // Still not canceled monthsOut months later counts as retained if (!s.canceledAt || s.canceledAt > boundary) cohorts[key].retained++; } return Object.entries(cohorts).map(([month, c]) => ({ month, retention: c.total ? Math.round((c.retained / c.total) * 100) : 0, }));}
Looking at this table every month, you start to see price changes and feature additions as lines — which signup month they began to work from. I once extended the trial from 7 to 14 days and conversion rose a few points from that month on; I only became sure of it once the cohorts were laid side by side. On a single total-revenue line, it would have been indistinguishable from seasonality.
Reducing churn isn't only a UI job. Finding the feature that low-retention cohorts have in common — the one they never touch — and rebuilding it does more than adding one more retention dialog. I'd rather use the numbers as a map of where to work next than as a scoreboard.
Legal and Compliance Essentials
If you sell subscriptions or IAP in Japan, three things are worth confirming before you write a line of code.
Publish the disclosure required under the Act on Specified Commercial Transactions: business name, address, contact, and refund policy on a page of their own. Store review can stall without it.
Make automatic renewal explicit. For recurring purchases, the final confirmation screen is expected to show when, how much, and how to cancel. The store purchase sheet covers much of this, but repeating it on your own in-app screen reduced refund inquiries noticeably for me.
Watch ad restrictions for children's apps. Apps aimed at users under 13 can show only a limited set of ads on AdMob, and a mismatch between the store age rating and ad settings can lead to rejection or a revenue hold.
Make One Model Work, Then Add the Next
The heaviest decision in monetization is choosing, first, which model fits your app. "Add everything" looks easy and turns out to be the hardest option. Narrow the candidates with the four axes, put your own assumptions into the estimator, make one model work, and only then layer the next — that order has been the realistic one for me.
If you do one thing today, drop your app's expected DAU, payer rate, and churn into estimateAds, estimateIap, and estimateSubSteadyState and look at the three numbers side by side. Which lever moves your app will become clear faster than any amount of looking at other people's revenue.
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.