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.

If you’re using platforms other than React Native (such as Flutter, Unity, Web, or iOS/Android natively), make sure your app is using the minimum supported SDK versions listed in the Virtual Currency documentation. SDK capabilities and supported methods can differ slightly between platforms.

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 CatalogVirtual 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

Using virtual currency with subscriptions

Virtual currencies can also be granted on a recurring basis through subscriptions. RevenueCat handles renewals, free trials, proration, and upgrades/downgrades automatically. If your subscription includes a virtual currency grant, a customer will receive the amount at the beginning of each billing period.
For full details on how grants behave across plan switches and trials, see the subscription section of the official documentation.

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. 

Virtual currencies are non-transferable and permanently tied to the Customer ID that receives them. Merging accounts, transferring balances, or moving currencies between users is not currently supported. If your app requires account linking or switching, ensure your identity logic is stable before granting any currency.

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 way we ensure the user is charged exactly one coin for this action, and keep 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.

Step 4: Test your virtual currency flow

Before you ship your app, you want to be confident that every sandbox purchase turns into the right number of coins, and that spending coins actually gates access to your AI features.

1. Configure sandbox behavior in RevenueCat

In your RevenueCat project settings (Project settings → General), you’ll find Sandbox Testing Access. This controls whether test purchases (RevenueCat Test Store and platform sandboxes like Apple / Google) actually grant entitlements and virtual currency.

You can choose between:

  • Anybody (default) – All sandbox purchases grant entitlements and add virtual currency as configured. This is perfect for early development when you’re iterating quickly.
  • Allowed App User IDs only – Only specific app user IDs you add to the allowlist will get entitlements and coins from sandbox purchases. Use this when you want to limit testing to a small set of QA accounts.
  • Nobody – Sandbox purchases are still recorded, but they no longer grant entitlements or virtual currency. This is handy if you want to test purchase flows without constantly polluting balances. 

If you tighten this setting (for example, switching from “Anybody” to “Allowed App User IDs only”), any entitlements granted by sandbox purchases to users who no longer qualify will be removed automatically. Virtual currency already granted will stay on the account, which is useful to remember if you’re doing lots of iterative tests against the same app user IDs. 

2. Run an end-to-end purchase test

With sandbox access configured:

  1. Create a fresh test account in the platform sandbox (Apple / Google, etc.).
  2. Log into your app with a dedicated app user ID (or an anonymous user if that’s your setup).
  3. Make a test purchase of your coin pack in the sandbox.
  4. Verify the balance:
    • In your app, call Purchases.virtualCurrencies() and confirm the balance matches what you expect.
    • In the RevenueCat dashboard, open the customer and check the virtual currency section to confirm the same balance. 

If the purchase succeeded but the balance didn’t change, double-check:

  • The product is associated with the correct virtual currency in the RevenueCat dashboard.
  • You’re looking at the correct app user ID in the dashboard.
  • Sandbox Testing Access isn’t set to “Nobody”.

3. Test spending and edge cases

Next, exercise the “spend coins” path:

  • Use your backend endpoint to deduct coins for a real action (e.g. “Generate 1 bed time story”), and confirm the balance decreases both in the app and the dashboard.
  • Try to spend more coins than you have, and ensure your backend correctly refuses the action and your UI shows an appropriate message.
  • Simulate network failures between your app and backend, and confirm you don’t double-charge or double-grant currency if the user retries.

Once you’re happy with the flow, consider switching Sandbox Testing Access from Anybody to Allowed App User IDs only so only your test accounts can keep manipulating balances in sandbox, while you continue building and demoing the app. 

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: