How to monetize your AI app with virtual currencies
A React Native tutorial for building an in-app virtual currency economy that buys bedtime stories, powered by LLMs

Summary
This tutorial shows you how to monetize your AI-powered mobile app using RevenueCat’s new virtual currencies feature. We’ll walk through the setup for a React Native app using RevenueCat to manage in-app purchases and track virtual currency balances, spending, and secure usage. You’ll learn how to configure currencies in RevenueCat, fetch and update balances.
Virtual currencies let you monetize AI-powered features, such as generating images or unlocking large language model (LLM) responses. They power in-app economies that boost retention and add flexible monetization options beyond subscriptions or one-time purchases.
In this guide, we’ll help you set up and integrate RevenueCat virtual currencies in your app. Here’s what we’ll cover:
- Creating virtual currencies in RevenueCat
- Associating products to sell currency
- Reading user balances from the SDK or backend
- Depositing and spending currency via your backend
- Security best practices for building trusted virtual economies
Prerequisites
Before starting, make sure:
- You have a RevenueCat account with a project created
- Your in-app purchases are set up and working in RevenueCat
- You are using:
- react-native-purchases 9.1.0 or higher
- react-native-purchases-ui 9.1.0 or higher
In this project we are going to use React Native, but virtual currencies are supported in all RevenueCat SDKs. Code samples are kept simple to make it possible to implement the shown examples in Swift, Flutter, and Kotlin when needed. The example project is forkable from this repository. The demo is a React Native app with Expo that generates cat themed bed time stories using Open AI’s API. Every generated story will cost one coin, and logic for calling both RevenueCat’s and Open AI’s APIs will be located in a custom function using Expo’s API routes.
Step 1: Set up virtual currencies in RevenueCat
The first step is to set up virtual currencies in the RevenueCat dashboard. Navigate to your project and go to the Product Catalog → Virtual Currencies.
Create a new product
Virtual currencies need to be associated with products that exist in App Store Connect and Play Store. Virtual currencies can be associated with both in-app purchases and subscriptions. For example you can make a consumable purchase which when bought gives the user 500 credits, or a subscription recurring monthly that gives 500 credits every month. In this tutorial we are going to use a consumable purchase for our single pack of 100 coins.
You can create products in App Store Connect through the RevenueCat dashboard. Navigate to Product catalog and select the Products tab. Click New product, and select your app. Give 100_coin_pack as the identifier, and 100 coins as the name, Product type should be Consumable. Once done click Create product.
RevenueCat has now created the product. You should see a prompt to create the product in App Store Connect. Click that, ensure that everything looks correct and click Create Product to create the product in App Store Connect.
You now have one consumable product to use with virtual currencies.
Create a new currency
Click + New virtual currency, and define:
- Code: COINS
- Name: Coins
- Description: Used to generate AI responses
- Icon: Optional
After adding all the details associate the product we created in the previous section. This is the only configuration we need to do in RevenueCat’s dashboard to start working with Virtual currencies
Step 2: Purchase virtual currency in app
To have functional virtual currencies we need to have both a mobile app that has RevenueCat SDK, and a backend to call which will handle deducting virtual currencies when a user makes a request to generate a new story.In this example, the backend is a simple Expo API route using Expo Router. We will be building both of these in this tutorial, starting with the app.
Add a paywall and check virtual currency balance
The first step is to configure the app to initialize RevenueCat, show the current balance of virtual currencies, and provide a paywall to purchase virtual currencies. Add the following changes to to your app, in the example project these are down on the first screen, index.tsx
1import { useEffect, useState } from "react";
2import { View, Text, TouchableOpacity, SafeAreaView } from "react-native";
3import Purchases from 'react-native-purchases';
4import Paywall from 'react-native-purchases-ui';
5
6export default function Index() {
7 const [coins, setCoins] = useState<number>(0);
8 const [error, setError] = useState<string>("");
9
10 const fetchCoins = async () => {
11 try {
12 await Purchases.invalidateVirtualCurrenciesCache();
13 const currencies = await Purchases.getVirtualCurrencies();
14 setCoins(currencies.all['COINS'].balance);
15 } catch {
16 setError("Could not load coin balance");
17 }
18 };
19
20 useEffect(() => {
21 Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
22 Purchases.configure({
23 apiKey: process.env.EXPO_PUBLIC_RC_SDK_KEY_IOS
24 });
25 fetchCoins();
26 }, []);
27
28 const handleBuyCoins = async () => {
29 const result = await Paywall.presentPaywall();
30 if (result === Paywall.PAYWALL_RESULT.PURCHASED) {
31 fetchCoins();
32 }
33 };
34
35 return (
36 <SafeAreaView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
37 <Text style={{ fontSize: 24 }}>Coins: {coins}</Text>
38 {error ? <Text style={{ color: 'red' }}>{error}</Text> : null}
39 <TouchableOpacity onPress={handleBuyCoins} style={{ marginTop: 20 }}>
40 <Text style={{ color: '#576cdb', fontSize: 18 }}>Buy Coins</Text>
41 </TouchableOpacity>
42 </SafeAreaView>
43 );
44}
This code initializes purchases in the useEffect so the SDK is ready to fetch the user’s virtual currency balance. Every time a user successfully completes the paywall the virtual currencies are refetched to update the cached amount of virtual currencies.
You also need to add .env file with the following keys:
1RC_PROJECT_ID=
2EXPO_PUBLIC_RC_SDK_KEY_IOS=
3RC_API_SECRET_KEY=
4OPENAI_API_KEY=
You can find RC_PROJECT_ID
from RevenueCat’s dashboard:
EXPO_PUBLIC_RC_SDK_KEY_IOS
is similar in the RevenueCat SDK. The EXPO_PUBLIC
part means that the key will be bundled with the app and therefore not a secret key that we have to be careful about leaking. However the other keys are secret and should never be visible in a code that someone could access for example by unbundling the app bundle.
The RC_API_SECRET_KEY
will come into use in the next section when we build the backend parts of this example. You have to generate it in the RevenueCat dashboard:
In this project we are using Open AI for generating bedtime stories. You can of course use any API you want or modify the following backend code to do as you wish. If you’re using Open AI, generate a new key in the Open AI API dashboard, and add it in the env for future use.
Call backend to generate a story
As a final step for the app itself we need to add a button for calling our backend to generate a story, and a Text element that will get populated with the generated story. When running locally the endpoint to call can be just /generate
and it will be called with a POST request that contains the customerID from RevenueCat’s SDK. Make the following changes to index.tsx
:
1import { useEffect, useState } from "react";
2import { View, Text, TouchableOpacity, SafeAreaView } from "react-native";
3import Purchases from 'react-native-purchases';
4import Paywall from 'react-native-purchases-ui';
5
6export default function Index() {
7 const [coins, setCoins] = useState<number>(0);
8 const [error, setError] = useState<string>("");
9
10 const fetchCoins = async () => {
11 try {
12 await Purchases.invalidateVirtualCurrenciesCache();
13 const currencies = await Purchases.getVirtualCurrencies();
14 setCoins(currencies.all['COINS'].balance);
15 } catch {
16 setError("Could not load coin balance");
17 }
18 };
19
20 useEffect(() => {
21 Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
22 Purchases.configure({
23 apiKey: process.env.EXPO_PUBLIC_RC_SDK_KEY_IOS
24 });
25 fetchCoins();
26 }, []);
27
28 const handleBuyCoins = async () => {
29 const result = await Paywall.presentPaywall();
30 if (result === Paywall.PAYWALL_RESULT.PURCHASED) {
31 fetchCoins();
32 }
33 };
34
35 return (
36 <SafeAreaView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
37 <Text style={{ fontSize: 24 }}>Coins: {coins}</Text>
38 {error ? <Text style={{ color: 'red' }}>{error}</Text> : null}
39 <TouchableOpacity onPress={handleBuyCoins} style={{ marginTop: 20 }}>
40 <Text style={{ color: '#576cdb', fontSize: 18 }}>Buy Coins</Text>
41 </TouchableOpacity>
42 </SafeAreaView>
43 );
44}
45
Step 3: Handle virtual currencies in the backend
Since virtual currencies are tied to users, we want the backend to handle monitoring the amount of virtual currencies the user has, and deducting them when a successful API call is done. This also improves the security as the user can’t just spoof the amount of virtual currencies and call the API with fake virtual currency amount.
In this case our “backend” is just an Expo API route, functionality that Expo router provides out of the box. Create a new API route by naming your file like generate+api.ts. Expo Router uses this convention to define API endpoints:
1import OpenAI from "openai";
2
3export async function POST(request: Request) {
4 const { customerId, currencyCode = 'COINS' } = await request.json();
5
6 if (!customerId) {
7 return Response.json({ error: 'Missing customerId' }, { status: 400 });
8 }
9
10 const balance = await getBalance(customerId, currencyCode);
11 if (balance < 1) {
12 return Response.json({ error: 'Not enough coins' }, { status: 402 });
13 }
14
15 const story = await generateStory();
16 await deductCoin(customerId, currencyCode, 1);
17
18 const remaining = await getBalance(customerId, currencyCode);
19 return Response.json({ story, remainingBalance: remaining });
20}
21
22async function getBalance(customerId: string, currency: string): Promise<number> {
23 const res = await fetch(
24 `https://api.revenuecat.com/v2/projects/${process.env.RC_PROJECT_ID}/customers/${encodeURIComponent(customerId)}/virtual_currencies`,
25 {
26 headers: {
27 Authorization: `Bearer ${process.env.RC_API_SECRET_KEY}`,
28 'Content-Type': 'application/json',
29 }
30 }
31 );
32 const data = await res.json();
33 return data.items.find((item: any) => item.currency_code === currency)?.balance || 0;
34}
35
36async function deductCoin(customerId: string, currency: string, amount: number) {
37 await fetch(
38 `https://api.revenuecat.com/v2/projects/${process.env.RC_PROJECT_ID}/customers/${encodeURIComponent(customerId)}/virtual_currencies/transactions`,
39 {
40 method: 'POST',
41 headers: {
42 Authorization: `Bearer ${process.env.RC_API_SECRET_KEY}`,
43 'Content-Type': 'application/json',
44 },
45 body: JSON.stringify({ adjustments: { [currency]: -amount } }),
46 }
47 );
48}
49
50async function generateStory(): Promise<string> {
51 const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
52
53 const res = await client.chat.completions.create({
54 model: "gpt-4.1-nano",
55 messages: [
56 { role: "system", content: "You're a calming cat storyteller." },
57 { role: "user", content: "Tell me a gentle cat bedtime story." }
58 ],
59 max_tokens: 500,
60 temperature: 0.7,
61 });
62
63 return res.choices[0]?.message?.content || 'No story available';
64}
65
Deducting virtual currencies in the backend
Let’s go through what the code does. After validating the customerId in the incoming POST request, we check the user’s virtual currency balance using the getBalance function. RevenueCat’s Web API can check the balance using the productID and customerID. The API returns a list of all virtual currency types the user holds, and we extract the balance for the specific currency. If the user doesn’t have at least one coin, we immediately respond with an HTTP 402 status, indicating that the request can’t be fulfilled due to insufficient balance.
If the user does have enough balance, we then proceed to generate a story by calling the generateStory function. This function uses the OpenAI SDK to create a short bedtime story about cats. We configure the model with a friendly system prompt that sets the tone, and a simple user prompt that asks for a gentle cat story. We extract the generated story from the response. You could also stream the response, but for the simplicity we just return the full response.
After generating the story, we deduct one coin from the user’s balance using the deductCoin function. Like the balance check, this also makes a call to RevenueCat’s REST API. This time, however, we send a POST request to the /virtual_currencies/transactions
endpoint with a negative adjustment for the COINS currency. This ensures the user is charged exactly one coin for this action, and keeps all transactions secure and server-side.
Finally, we re-fetch the updated balance to reflect the user’s remaining coins, and return both the generated story and the remaining balance in the response. This allows the frontend to update the story display and refresh the coin count without having to make an additional balance check on its own. Alternatively we can invalidate the virtual currency cache on the device with every call.
Conclusion
With just a few pieces of configuration and some simple backend logic, you’ve now integrated a complete virtual currency flow into your AI-powered app. Users can purchase coins through a paywall, track their balance in real time, and spend them on in-app actions like generating bedtime stories. By offloading the balance checks and deductions to the backend, you’ve also ensured that your virtual economy is secure.
Virtual currencies unlock a flexible and scalable way to monetize AI use cases, from story generation and chatbots to custom image creation and beyond. Whether you’re building a playful hobby app or scaling a serious product, RevenueCat’s virtual currencies give you the tools to engage users and drive revenue without the friction of traditional payment models. Now that you’ve seen how it all fits together, you’re ready to start experimenting, customizing, and building richer experiences around virtual currencies.
Learn more about virtual currencies:
You might also like
- Blog post
How to win Shipaton, part 1: coming up with an idea
Don’t know what to build for Shipaton? A 45x hackathon winner breaks down how to find and validate app ideas that solve real problems—and have a shot at winning.
- Blog post
Play Billing Library 8 support in Purchases SDK v9.0.0
Google Play Billing Library 8 introduces multiple purchase options for one-time products, non-expiring subscriptions, improved error handling, and removes support for querying expired subscriptions and consumed products.
- Blog post
A Beginner’s guide to implementing an ad-free subscription in your Flutter app
A step-by-step tutorial to let users pay to remove ads—using AdMob, and RevenueCat