I built a chatbot that sends you memes, digs up gifs, and roasts you. It's called MemeChatAI, and it is, very much on purpose, brainrot. You talk to it, it talks back in the dumbest, funniest way it can manage. That's the whole pitch.
So here's the thing nobody warns you about when you build a joke app: the joke is free, but charging money for it is a real engineering project. The bot was the fun weekend part. Billing was the part that could actually lose people's money if I got it wrong, and that's the part I want to talk about, because RevenueCat ate most of it for me.
The problem I did not want to own
If you've never shipped in-app subscriptions before, here's the short version of what you're signing up for. Apple has its own receipts and renewal rules. Google has different ones. Both need server-side validation if you want to trust anything. Then there's restoring purchases, upgrades, downgrades, free trials, grace periods when someone's card bounces, and the lovely edge case where a user buys on their iPhone and then logs in on an Android tablet and expects their stuff to be there.
I did the math on building all of that myself and decided I'd rather not. For a roast bot. I'm not proud, but I'm also not sorry.
RevenueCat is basically the layer that sits between your app and both stores and turns all of that into one SDK and one webhook. That's the elevator pitch, and in my case it mostly held up.
One SDK, both stores
The app is React Native on Expo, using react-native-purchases (v10). The same code path runs on iOS and Android. Apple's StoreKit and Google's Billing Client are both hiding behind one API, so I'm not maintaining two native billing integrations that drift apart over time.
Prices, products, and the trial all live in the RevenueCat dashboard, not in my code. The app pulls them down as "offerings" and renders whatever comes back:
const offerings = await Purchases.getOfferings();
const pkg = offerings.current.availablePackages[0];
await Purchases.purchasePackage(pkg);
Nothing is hardcoded. Pricing localizes per region automatically, and "Restore Purchases" is one line:
await Purchases.restorePurchases();
The quiet win here is that I can change a price or tweak the trial from a dashboard without shipping an app update or sitting in a review queue for two days. Pricing is config now, not code. That alone has saved me more than once.
Entitlements are the only source of truth
This is the part that made the rest sane. Instead of my app trying to reason about raw receipts, everything resolves to a single entitlement called pro. From there I map RevenueCat's products onto four internal tiers: free, basic, plus, and power. That mapping lives in one place and is shared between the client and the server, so the two can't disagree about what a "plus" user is allowed to do.
There's also a listener that fires the moment anything changes:
Purchases.addCustomerInfoUpdateListener((info) => {
updatePlanFromEntitlements(info.entitlements.active);
});
Buy, renew, upgrade, cancel: the UI flips over without a refresh or a manual poll. The first time I tested an upgrade and watched the higher tier just appear, I'll admit it felt a little magic.
Purchases follow the account, not the phone
On login I tie the RevenueCat customer to the Firebase user:
await Purchases.logIn(firebaseUid);
That one call is why "buy it on your iPhone, use it on your Android" works at all, and why my backend can always trace a purchase back to the right profile. I genuinely did not appreciate how annoying this is to do by hand until I didn't have to.
The webhook is where I stopped trusting the client
The client is fast but it's also a liar sometimes. Networks drop. People background the app mid-purchase. So the actual billing record lives server-side.
A Cloud Function listens for RevenueCat webhook events (INITIAL_PURCHASE, RENEWAL, PRODUCT_CHANGE, EXPIRATION, CANCELLATION, BILLING_ISSUE, TRANSFER, and friends) and writes the user's plan to the database. RevenueCat is the source of truth, full stop.
A few things I made sure of, because billing bugs are the worst kind of bug:
- Events are idempotent. I dedupe by event ID, so a retry can't apply the same upgrade twice.
- Writes are transactional.
- Sandbox and test events are gated out of production, so my own testing never touches a real user's plan.
The client still does an optimistic "you're upgraded, go enjoy it" write so the app feels instant. But there's a rank guard: a stale client can never downgrade a plan that the webhook authoritatively set. The webhook always gets the last word and reconciles. I lost an afternoon to a race condition before I added that guard, and I'd rather you not.
The stuff I got for free
A pile of things I would have built badly came included:
The 7-day free trial is tracked off RevenueCat's trial signals, so I know the difference between someone in trial and someone who actually converted. Upgrades grant the new tier immediately; downgrades wait politely until the current billing cycle ends. Billing issues and grace periods are RevenueCat's problem to manage, and my app just logs and waits. When someone wants to cancel, I hand them the native Apple or Google "manage subscription" screen instead of pretending I should be in the middle of that.
There's also a test store for local dev, so I can run the whole purchase flow without standing up real App Store and Play products first. For iteration speed that's huge.
Is there a catch? Sort of.
I want to be honest, because I find "this tool is perfect" posts useless. Handing your billing source-of-truth to a third party is a real dependency, and I thought hard about it. If RevenueCat has a bad day, my purchases have a bad day. There's a cost once you cross their free tier, and you're trading some control for all this convenience.
For a solo-ish project shipping a meme bot to two stores, that trade was obviously worth it. For a company whose entire business is subscriptions at massive scale, I'd at least want to think about it longer. Your call.
One accuracy note while I'm here: RevenueCat doesn't see or store your card. Apple and Google run the actual transaction. RevenueCat manages entitlements on top of that. I'd been fuzzy on this myself before I read the docs, so I'm spelling it out.
So
The dumb part of MemeChatAI took a weekend. The billing took real care, and most of that care went into the half-page of webhook logic above, not into reimplementing two stores' worth of receipt validation. That's the trade I'd make again.
If you want to get roasted by a bot, it's on the App Store, and there's more at meme-chat-ai.com. Fair warning: it has no manners. That's a feature.
Top comments (0)