← Plugin catalog
Developer Tools
RevenueCat
RevenueCat v2.3.0
Publisher description
From the marketplace listing
Set up RevenueCat, integrate it in your apps, and access and analyze data about your customers, revenue, purchases.
Language: English · Automatically detected from descriptions.
Files & skills
File archives
Plugin package71 files · 106 KBBrowse files →
Skill instructions
create-revenuecat-project3.96 KB
---
name: create-revenuecat-project
description: "Set up a complete RevenueCat project from scratch — creates apps, products, entitlements, offerings, and packages in the correct order. Use when the user wants to create a new RevenueCat project, configure in-app purchases, set up subscriptions or monetization, or bootstrap IAP infrastructure for iOS, Android, or Web."
---
# RevenueCat Project Bootstrap
Guide through setting up a complete RevenueCat project from scratch.
## Instructions
**Important:** Use the RevenueCat MCP server or the `rc` CLI (whichever the user has available) for all operations below; both cover the same actions for these steps. See the `revenuecat-cli` skill for CLI discovery and conventions. Always list projects first (`list-projects` or `rc projects list`) to retrieve all accessible projects. If multiple projects are returned, ask the user which project to use or if they want to create a new one.
### Phase 1: Discovery
Ask targeted questions to understand the developer's needs:
1. **Platforms** — "Which platforms are you building for?" (iOS, Android, Web, or multiple)
2. **Business Model** — "What type of monetization are you planning?" (subscriptions, one-time purchases, consumables, or a mix)
3. **Subscription Tiers** (if applicable) — "What subscription options do you want to offer?" (common: Monthly + Annual, single tier, Freemium + Premium)
4. **App Details** — Bundle ID (iOS, e.g. `com.company.appname`), package name (Android), and display name
### Phase 2: Create Resources
Execute in this order — dependencies matter. Each step names the MCP tool; the equivalent `rc` commands are `rc projects create`, `rc apps create`, `rc products create`, `rc entitlements create`, `rc entitlements attach`, `rc offerings create`, `rc packages create`, `rc packages attach`, and `rc apps keys`.
1. Verify/Create Project
`list-projects` - list accessible projects
If multiple: ask user which to use, or offer to create a new one
To create a new project, use the `create-project` MCP tool
Store project_id for all subsequent calls
2. Create Apps (for each platform):
- For mobile apps, ask if the user already has set up their app in App Store Connect / Google Play Console. If so, create an app using the `create-app` tool (type: app_store | play_store). If not, use the automatically generated `test_store` app and tell the user that they can set up the integration with App Store Connect / Google Play Console later.
- For web apps, `create-app` with type rc_billing
3. Create Products (for each subscription/purchase): `create-product` tool
4. Create Entitlements (for each feature/access level): `create-entitlement` tool
5. Attach Products to Entitlements: `attach-products-to-entitlement` tool
6. Create Default Offering: `create-offering` tool (lookup_key: "default")
7. Create Packages in Offering: `create-package` tool (for subscriptions, use $rc_monthly, $rc_annual, etc.)
8. Attach Products to Packages: `attach-products-to-package` tool
9. Get API Keys: `list-app-public-api-keys` tool
### Phase 3: Summary & Next Steps
Provide a complete setup summary:
```
Project Setup Complete!
=======================
Project: {project_name} ({project_id})
Apps Created:
iOS: {app_name} - API Key: appl_xxxxx
Android: {app_name} - API Key: goog_xxxxx
Products:
- monthly_premium (subscription, P1M)
- annual_premium (subscription, P1Y)
Entitlements:
- premium → monthly_premium, annual_premium
Offering: default (current)
└── $rc_monthly → monthly_premium
└── $rc_annual → annual_premium
Next Steps:
1. Configure store credentials in RevenueCat dashboard
2. Create products in App Store Connect / Play Console
3. Add SDK to your app (see /rc:create-app)
4. Implement paywall UI using the "default" offering
```
## Error Handling
If any step fails:
1. Report the specific error clearly
2. Suggest fixes (e.g., "Bundle ID may already be in use")
3. Offer to retry or skip that step
4. Continue with remaining steps if possible
experiment-analysis4.93 KB
---
name: experiment-analysis
description: Use when the user asks to analyze or understand a RevenueCat experiment or its results.
---
To analyze an experiment, follow the following steps. Make sure to execute all steps in this order, do not skip any. More details on the step below. If you already have partial information (eg. the experiment ID), you may skip that step only. Continue following all of the other steps.
1. Get the experiment ID
2. Get experiment details
3. Get supporting chart data [DO NOT SKIP]
4. Get experiment results
5. Interpret results
6. Report your overall findings
# Step 1: Get the experiment ID
If you don't have the experiment ID yet, find it using the `list-experiments` RevenueCat tool. It receives a single parameter: `project_id`, which requires the `proj...` Project ID. You can also pass a `status` filter (`draft`, `running`, `paused`, `stopped`).
# Step 2: Get experiment details
## Step 2a: Experiment setup
Get the experiment setup / metadata by using the `get-experiment` RevenueCat tool. Parameters: `project_id` (as above) and `experiment_id` (from step 1). Pass the following values to the `expand` parameter: `offering.package.product.indicative_price` and `offering.paywall`.
Relevant information to extract:
- Offerings (offering_a, offering_b, ...): these define the variants of the experiment. More sophisticated setups might also include a `placements` object which defines different offerings per placement (eg. onboarding, feature gate, ...). Offerings include details on the products offered. Products include an `indicative_price`. Note that only USD prices for the US are returned to provide an understanding of overall pricing levels. Any introductory price is not returned. For price localization tests, tests scoped explicitly outside of the US, or introductory prices, this will not provide the full picture, you might have to resort to the `get-product-store-state` tool instead (see the `revenuecat-store-state` skill). Offerings may also include a `paywall_id`, if the offering uses a RevenueCat Paywall. If `paywall_id` is `null`, that means the app is using a custom paywall.
- notes: Any notes that were provided about the experiment
- display_name: Name of the experiment
- targeting_conditions: the experiment only applies to customers meeting these conditions
- enrollment_mode: defines whether only new customers are enrolled (default) or whether this experiment also applies to existing customers
- experiment_type: what kind of experiment this is (user selected out of a predefined list)
- primary_metric, secondary_metrics: primary and secondary success metrics as set up when creating the experiment.
- Status:
- `draft` means the experiment has not yet started, and there are no results yet.
- `running` means the experiment is still actively enrolling new customers.
- `paused` means the experiment is no longer enrolling customers, but already-enrolled customers are still assigned to their variant, and the experiment is continuing to collect data for up to 400 days. The experiment can be resumed to continue enrolling new customers.
- `stopped` means the experiment is no longer enrolling customers, and already-enrolled customers have gone back to being served their default offering. Results continue to refresh for up to 400 days. The experiment can no longer be started or resumed.
## Step 2b: Paywalls
For any paywall_id in any offering of the experiment, use the `render-paywall-screenshot` tool (parameters `project_id`, `paywall_id`) to look at the paywall and understand the differences.
# Step 3: Get supporting chart data
Pull recent chart data scoped to the experiment's runtime, using the `get-chart-data` RevenueCat tool. Do not skip this step, it provides valuable context to how the app was performing overall in the time frame.
Parameters:
- project_id: {project_id}
- chart_name: revenue
- start_date: {experiment_start_date}
- end_date: {experiment_end_date}, or today if still running
- resolution: week (or day if < 2 weeks running)
Also pull data for chart_name :
- conversion_to_paying — overall conversion trend during experiment
- trials_new — trial volume trend
- trial_conversion_rate — trial-to-paid during experiment
- initial_conversion – Initial conversion rate (conversion from install to trial, upfront subscription, or one-time purchase)
# Step 4: Get experiment results
Get the experiment results (so far) using the `get-experiment-results` RevenueCat tool.
Parameters:
- project_id
- experiment_id
- platform (optional), filter results by platform, eg. `ios`
- country (optional), filter results by country, eg. `us`
- exposure_status (optional), filter experiment results by exposure status. One of `enrolled`, `exposed`, `not_exposed`. Only applicable for experiments for existing customers. Defaults to "enrolled" (all enrolled customers) when not provided.
# Step 5: Interpret results
Understanding what changes were made in the experiment, now interpret the results. Use your best judgment.
integrate-revenuecat9.13 KB
---
name: integrate-revenuecat
description: End-to-end RevenueCat integration — sets up the dashboard side via the RevenueCat MCP or the `rc` CLI (project, app, public API key) and installs/configures the Purchases SDK in the app. Use when the user asks to add RevenueCat, integrate Purchases, install the RevenueCat SDK, set up a RevenueCat API key, configure Purchases on launch, or set up a brand new RevenueCat integration on iOS, Android, Kotlin Multiplatform, Flutter, or React Native.
---
# integrate-revenuecat: end-to-end RevenueCat integration
Use this skill when the user wants to add RevenueCat to a project for the first time, or to reconfigure the SDK with a public API key. The skill covers two halves:
1. **Dashboard side** — set up the project, register the app, and obtain the public API key, through the RevenueCat MCP server or the `rc` CLI.
2. **App side** — install the Purchases SDK, call `Purchases.configure(…)` at app entry, and verify the configuration banner in the logs.
Walk them in order. Most integrations need both halves, even when the user asks "just install the SDK" — the SDK needs an API key from the dashboard.
> If a project + app already exist and the user only wants to wire the SDK into code, jump to **Section 3** below.
> If the user wants to bootstrap a brand new RevenueCat project (apps + products + entitlements + offerings), use the `create-revenuecat-project` skill instead, then come back here for the SDK install.
## Arguments
Available as `$ARGUMENTS` when invoked as a slash command:
- `platform` (optional): One of `ios`, `android`, `kmp`, `flutter`, `react-native`. If omitted, run the detection algorithm in Section 3a.
- `app_identifier` (optional): Bundle ID (iOS) or package name (Android). If omitted, read it from the project files (`Info.plist`, `AndroidManifest.xml`, `app.json`, `pubspec.yaml`).
- `project_name` (optional): Name of the RevenueCat project to use. If omitted, list projects (via MCP or `rc projects list`) and ask the user.
## 1. Understand the status quo
Before touching the dashboard, gather the facts:
- **Platform target**: iOS / Apple App Store, Android / Google Play, or both. Inspect the working directory before asking — the detection algorithm in Section 3 makes this obvious for most projects.
- **Technology**: native iOS (Swift), native Android (Kotlin / Java), React Native, Flutter, Kotlin Multiplatform. SDK list: https://www.revenuecat.com/docs/getting-started/installation.md.
- **App identifier**: bundle ID (iOS), package name (Android). Pull from `Info.plist` / `AndroidManifest.xml` / `app.json` / `pubspec.yaml` rather than asking.
## 2. Dashboard side
Use whichever surface the user has available: the RevenueCat MCP server or the `rc` CLI (see the `revenuecat-cli` skill for CLI discovery and conventions). Both cover the same operations for these steps, and each step below notes the MCP tool and the `rc` command.
### 2a. Get or create the project
- List accessible projects: `list-projects` (MCP) or `rc projects list`. If multiple, ask the user which one matches this app, or offer to create a new one (`create-project` / `rc projects create`).
- If there is no project, hand off to the `create-revenuecat-project` skill, then resume here.
- Store the `project_id` for the rest of the steps.
### 2b. Get or create the app
- Check which apps are already configured in the project (`list-apps` / `rc apps list`). A `test_store` app is always present; `app_store` and `play_store` apps are present only if the user has finished store-side setup.
- Ask the user whether their app is already set up in App Store Connect (iOS) or Google Play Console (Android). Reassure them that store-side setup can come later — the `test_store` app is enough to start integrating.
- If the user confirms store-side setup is done, call `create-app` (or `rc apps create`):
- **iOS**: `type: "app_store"`, `bundle_id` from Section 1.
- **Android**: `type: "play_store"`, `package_name` from Section 1.
- `name` derived from the identifier or asked from the user.
### 2c. Get the public API key
- List public keys for the relevant app ID: `list-public-api-keys` (MCP) or `rc apps keys <app-id>`:
- `app_store` / `play_store` if the store-side app exists.
- Otherwise the `test_store` app.
- The returned key is **public** and safe to embed in client app code. iOS keys are prefixed `appl_…`, Android keys `goog_…`, Amazon `amzn_…`.
> **Never use the secret API key in client code.** Secret keys are server-side only.
## 3. App side — install and configure the SDK
### 3a. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency → read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root → read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*` → read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP) → read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root → read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
### 3b. Shared concepts (all platforms)
- **Public SDK key, not secret key.** RevenueCat issues a separate public SDK key per store/platform. iOS apps use an `appl_…` key, Android apps use a `goog_…` key (Amazon uses `amzn_…`). Server-side secret keys must never appear in client apps.
- **Configure once per app launch.** Call `Purchases.configure(…)` exactly once, as early as possible (app entry point). Later calls no-op or warn.
- **Anonymous users by default.** If you don't pass an `appUserID`, RevenueCat creates a stable anonymous ID. Only pass `appUserID` if you already have an authenticated user at launch; otherwise call `logIn(…)` later (see the `revenuecat-identify-user` skill).
- **Enable debug logging during integration.** Each platform file shows how. Turn it off for release builds.
- **Keep keys out of source control.** Recommend `.env` (RN), `xcconfig` (iOS), `local.properties` / `gradle.properties` (Android), or dart-define (Flutter) when the user asks about secret management.
### 3c. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
Each platform file is self-contained: install command, exact `configure` snippet, and where to place it in the app entry point.
## 4. Verify
Do not claim setup is complete until:
1. The project **builds** (Xcode build, `./gradlew assembleDebug`, `flutter run`, `npx react-native run-ios`, or the KMP equivalent).
2. The app launches and the RevenueCat SDK logs a configuration banner in the console / logcat / Metro output (each platform file describes the expected log line).
3. No authentication errors appear on the first SDK network call. A wrong API key surfaces as an auth error log as soon as the app fetches offerings.
If the user only asked to "install" without running the app, tell them what to look for in the logs when they do run it.
## 5. Next steps
### 5a. Products, entitlements, offerings
Check whether products, entitlements, and offerings are already set up in the project. If not, offer to help via the `create-revenuecat-project` skill.
### 5b. Store-side setup
**iOS (App Store Connect)**
1. **In-App Purchase Key (recommended for StoreKit 2)** — App Store Connect → Users and Access → Integrations → In-App Purchase. Generate key, download the `.p8` file. Note the Key ID and Issuer ID.
2. **Shared Secret (legacy StoreKit 1)** — App Store Connect → App → App Information → App-Specific Shared Secret.
3. If the user provides this information, register it on the RevenueCat side via `create-app` / `update-app`.
**Android (Google Play Console)**
1. **Service account credentials** — Create a service account in Google Cloud Console. Grant "Service Account User" role. Create a JSON key. In Play Console, grant the service account access with "View financial data" permission.
2. **Real-time Developer Notifications (RTDN)** — Set up a Cloud Pub/Sub topic. Configure in Play Console → Monetization setup.
3. If the user provides this information, register it via `create-app` / `update-app`.
### 5c. Subsequent skills
Common follow-ups after `integrate-revenuecat`:
- `revenuecat-paywall` — display a dashboard-configured paywall.
- `revenuecat-purchase-flow` — implement purchase + restore manually.
- `revenuecat-entitlements-gate` — gate features behind active entitlements.
- `revenuecat-identify-user` — wire `logIn` / `logOut` to the app's auth system.
- `revenuecat-testing-setup` — set up a sandbox testing channel.
- `revenuecat-troubleshoot` — diagnose offerings / products / entitlement bugs.
Referenced files: 5
revenuecat433 Bytes
--- name: revenuecat description: Used for all interaction with RevenueCat not covered by a different skill --- Use the RevenueCat MCP server or the `rc` CLI for all interaction with RevenueCat. See the `revenuecat-cli` skill for the CLI's commands and conventions. Refer to the RevenueCat docs under https://www.revenuecat.com/docs. You can find markdown versions of individual pages by appending a `.md` to the URL of a docs page.
revenuecat-app-valuation16.9 KB
---
name: revenuecat-app-valuation
description:
Use this skill when the user asks how much their app is worth, what they could sell their app for,
how app valuations or acquisitions work, or how to prepare a subscription app for sale/exit.
---
# App valuation and selling
Help a founder understand what their app might be worth and how app acquisitions work. The
frameworks below come from RevenueCat's guidance on selling apps, extended with how buyers
reason about risk. Ground every estimate in the user's own RevenueCat data where you can, rather
than quoting the ranges in the abstract.
Two rules hold for this topic:
- Always present valuation as a range with a confidence level, never a single guaranteed number.
An app is ultimately worth what a buyer will pay, and there is no guarantee a buyer exists at
any given price.
- Treat this as general education, not financial, legal, or tax advice. For an appraisal, tax
treatment, or deal terms, tell the user to consult a broker, attorney, or tax professional.
## The method
A valuation is four decisions, in order. Work through them explicitly.
1. Choose the profit base, and name it.
2. Choose the band that base earns.
3. Position the app within that band on the evidence.
4. State the range, the confidence, and what would move it.
## 1. Choose the profit base
Apps are valued as a multiple of profit, not revenue. Every multiple in this skill is a profit
multiple. Do not apply multiples directly to revenue metrics.
| Base | Multiple |
| ------------------------ | ------------------------------------- |
| Monthly operating profit | 12x–36x (broad), 36x–60x (mature sub) |
| Annual operating profit | 1x–3x (broad), 3x–5x (mature sub) |
MRR and ARR still matter — they are the top line the profit is computed from, and the input to
the retention, growth, and concentration checks below. They are not a valuation base.
When the user knows revenue but not costs, ask for costs. If they cannot give them, state the
assumed margin as an explicit assumption and show the arithmetic in full — "$200k ARR × 60%
assumed margin = $120k profit × 3–5x = $360k–$600k" — so the user can correct the assumption.
### Owner earnings or post-hire profit
There are two defensible profit bases, and they are not interchangeable. Pick one, name it in the
answer, and apply the matching multiple.
- Owner earnings — what a solo buyer who does the work themselves would take home. Proceeds minus
cash operating costs, plus one-off and non-recurring costs added back. Use for owner-operated
apps sold to individual buyers.
- Post-hire operating profit — what the app earns after paying market rate for the work the
founder does for free today. Owner earnings minus the cost of replacing that work. Use when
the buyer is a studio or portfolio acquirer who will staff the app. Default to this.
The published 3x–5x band is a post-hire number: the acquirers quoting it are portfolio buyers who
carry the operating cost.
To size replacement cost, ask how many hours a week go into the app across engineering, support,
marketing, and admin; who is paid today and how much; and whether the founder intends to stay
through a transition. A part-time maintenance load is a few thousand dollars a month of
replacement cost; a full-time founder doing engineering and user acquisition is a salary. Present
it as a stated assumption, not a derived fact.
### Computing the number
Start from proceeds — revenue net of sales tax/VAT and store commissions — rather than gross
revenue, since App Store / Play Store fees are already removed. Request proceeds from
`get-chart-data` on the `revenue` chart with the `revenue_type` selector set to `proceeds` (see
`revenuecat-charts`). Then subtract the expenses that directly keep the app running: operational
costs (servers, customer support, licensing), marketing and user-acquisition spend, and ongoing
maintenance. Apply the add-backs above.
- Do not subtract interest or corporate income tax — those reflect the seller's financing and tax
situation, which the buyer replaces with their own. The "taxes" already removed in proceeds is
sales tax/VAT; don't subtract income tax on top.
- Development costs for building _new_ features are usually excluded — they fund future profit,
not the maintenance of existing profit. Every app differs, so note when some development cost
belongs in the number.
- Use trailing-twelve-month (TTM) figures as the base.
Then compute the last two full quarters annualized as a run-rate sensitivity. When it differs
from the trailing-twelve-month base by more than about 20%, present both:
> On trailing-twelve-month profit of $100k, the range is $300k–$500k. At the current run rate of
> $140k, the same multiples give $420k–$700k. Buyers will anchor closer to the trailing figure
> and ask you to prove the run rate holds — sustained growth is the argument for the top of the
> range, not for a bigger base.
This applies in both directions: for a declining app, the run-rate number is the floor a buyer
will push toward and the trailing figure is what the seller defends. Show the gap either way. A
run-rate sensitivity is not license to annualize a spike — two or more quarters of consistent
movement is a trend, one strong month behind a launch or a seasonal peak is not. Check chart
`annotations` before calling something a trend.
## 2. Choose the band
RevenueCat publishes two ranges, and they are not the same range — they describe different tiers
of app:
- Broad-market heuristic: apps typically sell for 12x–36x monthly operating profit (≈ 1x–3x
annual). This spans the whole market, including younger or higher-churn apps. Some sell for
many multiples more, and some do not sell at all.
- Mature-subscription floor: for a proven, low-risk subscription business, disciplined buyers
anchor at 3x–5x annual operating profit (≈ 36x–60x monthly). Worked example: an app earning
$100k annual operating profit at 3–5x is worth roughly $300k–$500k.
These bands sit end-to-end, meeting only at 3x annual / 36x monthly. Do not average the two or
claim a single number reconciles them.
### The mature-subscription band needs evidence, not age
Qualification is about whether a full renewal cycle is observable, which is usually visible well
before an app's second birthday. Check all three:
- Subscription-dominant revenue — roughly 70%+ of proceeds from subscriptions.
- A completed renewal cycle for the dominant plan duration. Read `subscription_retention` or
`cohort_explorer`: an annual-dominant app needs cohorts observed past month 12, so the first
renewal is in the data. A monthly-dominant app needs cohorts observed well past the first-month
churn cliff — several renewal periods, not one. Check the annual-vs-monthly mix by revenue
first, since it decides which threshold applies. An 18-month-old annual-dominant app with
complete month-12 cohorts qualifies; a three-year-old app that just launched annual plans does
not.
- A curve that has flattened. Retention should decay and then level off across at least two
consecutive cohorts. A curve still falling steeply at the last observed period is incomplete
evidence however long the history is.
Fail any check and the broad-market band applies. Say which check failed and what would clear it
— usually a specific number of months.
Past the gate, age is not a factor: the multiple is set by what the cohorts show.
### Apps that are not subscription-dominant
RevenueCat currently has no published or recommended valuation insights for apps that are not
predominantly subscription apps. When the app you are asked about is not predominantly
subscription based, do not offer a valuation.
## 3. Position within the band
The band is the starting point; these factors set where in it the app lands. Score each factor
strong, neutral, or weak, and say which evidence drove each call.
| Factor | Strong | Weak | Where to look |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------- | ---------------------------------------------------------- |
| Retention (2x weight) | Churn/retention in the top 30% of category peers, flat curve | Bottom 30%, or a still-falling curve | `get-benchmarks` (monthly churn), `subscription_retention` |
| Trajectory | Growing 25%+ YoY, sustained two or more quarters | Declining 10%+ YoY | `revenue` (proceeds), trailing twelve vs prior twelve |
| Monetization | Realized LTV, trial and paying conversion in the top 30% | Bottom 30%, or no pricing headroom left | `get-benchmarks`, `conversion_to_paying` |
| Distribution | Compounding channels, healthy paid payback | Borrowed growth, or paid that does not pay back | Ask the user |
| Concentration | Diversified across store, geography and SKU | A single dependency past the thresholds below | `revenue` segmented by `store`, `country`, `product_id` |
| Transferability | Low ops burden, documented, transferable store account | Founder-dependent, undocumented, transfer-blocked | Ask the user |
Use `get-benchmarks`' `percentile_bucket` directly: 70+ is strong, 30–70 neutral, under 30 weak.
It already accounts for reverse metrics, so a high percentile on churn means low churn. Rows with
`is_eligible_for_benchmarking: false` are low-confidence — score them neutral and say so.
Then position:
- Net two or more strong, no weak — top third of the band.
- Roughly balanced — middle third.
- Net two or more weak — bottom third.
Additional factors that cap what a buyer is willing to pay: declining revenue, issues that might
prevent transfer of the app, undocumented financials, concentration (see below). Mention any
that apply.
Show the scorecard, not just the conclusion. "Top-quartile retention and 30% year-over-year
growth, but 90% of revenue from one country" is the answer; the number summarizes it.
### Reading the factors
- Retention / subscriber stickiness — the strongest signal of product-market fit and future cash
flow, and the biggest lever on the multiple, which is why it carries double weight. What counts
as "good" is category-dependent: buyers cited healthy annual-subscription retention anywhere
from ~25% to ~70%. Benchmark against the app's category rather than a universal number, and be
ready to explain early churn (roughly 30% of subscriptions cancel in the first month) and show
that later cohorts stabilize.
- Monetization headroom — under-monetization is a plus to buyers: proven monetization with clear
room to grow (pricing, offers, plan mix).
- Distribution durability — treat acquisition channels as a quality tier, not a preference:
- Compounding (strongest): organic ASO, SEO, brand, and word of mouth.
- Rentable: paid user acquisition. Check payback period, not just volume.
- Borrowed (weakest): UGC, viral loops, influencer-driven and algorithm-driven growth, a
single creator, a store feature. Discount it heavily, and ask what happens to installs if it
stops tomorrow.
Buyers do differ on the organic/paid balance (cited preferences run from 100% organic through
50/50 to 75/25 paid/organic), so a defensible mix beats a dogmatic one.
- Concentration — check the top segment's share of proceeds by `store`, `country` and
`product_id` over the trailing twelve months:
- Platform/store. Being iOS-only is common and only a mild discount on its own.
- Geography. One country above ~70% of revenue is a real cap.
- SKU / product. One product above ~70% of revenue means the business rests on one price point.
- Beyond the charts: a single traffic source, a single integration, and — for B2B-shaped apps
— a handful of accounts carrying revenue.
- Technical infrastructure — clean architecture, low operating burden, and good documentation
reduce perceived risk and are most of the transferability score.
### Outside the scorecard
These change the deal rather than the app's position in its band. Raise them, but do not bake
them into the range:
- Strategic / portfolio fit — the best price often comes from the buyer who sees the most
synergy.
- Founder involvement — most buyers view the founder staying through a transition as a positive.
Large studios with in-house teams are the exception.
- Deal structure — offers come as cash (most common), equity, or earnouts. Roughly two-thirds of
offers are pure cash. Cash deals often pay ~50% at close and the remainder over the following
6–12 months.
## 4. State the range and the confidence
Present the result as: named profit base × low-end multiple to × high-end multiple, the position
within the band and why, then a confidence level.
Confidence is low with thin or lumpy data, when the factors conflict, when several inputs are
user-supplied and unverifiable, or when a large share of revenue is non-recurring. It is higher
with 12+ clean months, low churn, a completed renewal cycle, and diversified revenue. A wide,
honest range beats a tight one you cannot defend.
## Grounding an estimate in the user's RevenueCat data
When the user wants an estimate for their own app, pull their real metrics first.
- Use `revenuecat-charts` to fetch what the steps above need. Prioritize: proceeds (trailing
twelve months and by quarter), subscription retention by cohort, monthly churn, trial and
paying conversion, ARPU, annual-vs-monthly plan mix, realized LTV, and proceeds segmented by
store, country and product.
- Compare against category benchmarks via `get-benchmarks` (realized LTV, trial conversion,
initial conversion, conversion to paying, monthly churn, refund rate). Read the percentiles
into the scorecard as described in step 3.
- RevenueCat has no acquisition-cost data, so it cannot compute CAC, LTV/CAC, or payback period
on its own. Do not present a CAC-based number without user-supplied acquisition data.
- Link the user to the relevant charts with `revenuecat-charts` (e.g. MRR, churn, subscription
retention) so they can see the inputs behind the estimate.
Several inputs are not in RevenueCat at all. Ask for them together, once, rather than one at a
time:
| Input | Needed for |
| ----------------------------------------------- | ------------------------------ |
| Operating costs (servers, support, tooling, UA) | The profit base |
| Founder and team hours, and who is paid | Add-backs and replacement cost |
| Ad revenue, by network | Valuing non-recurring streams |
| Channel split, UA spend, payback period | Distribution durability |
| Ops burden, documentation, transferability | Transferability, and hard caps |
## Preparing to sell, finding buyers, negotiating
If the user is moving toward an actual sale, summarize the relevant parts and point them to the
full posts for detail:
- Can they even sell it? Most App Store apps can be transferred between developer accounts; a
few cannot (e.g. conflicting in-app-purchase product IDs, some sandboxed Mac apps, Apple Arcade
apps), in which case the whole developer account transfers instead. Direct them to Apple's
app-transfer criteria to confirm.
- Prep before talking to buyers: clean, reconciled financials, a clear retention/growth story,
tidy metrics, low-friction product and infrastructure, and handover documentation.
- Finding buyers: marketplaces and brokers (e.g. Flippa, AppFlip, App Business Brokers) and
direct acquirers exist; warm intros generally outperform cold outreach.
- Negotiating: the first offer often lowballs. Recommend independent legal/financial advice for
the actual transaction.
## Citing sources
Base answers on RevenueCat's own material and cite it. The two primary posts are:
- "How to sell your mobile app" — https://www.revenuecat.com/blog/growth/how-to-sell-an-app/
- "What app buyers really want: insights from 10 leading acquirers" —
https://www.revenuecat.com/blog/growth/guide-to-selling-apps/
Attribute the two published bands, the retention ranges, the channel-preference splits, and the
deal-structure figures to those posts. The factor scorecard, the concentration thresholds, the
add-back treatment, and the handling of non-recurring revenue are general market practice rather
than RevenueCat findings — present them as how a buyer reasons, and do not cite a RevenueCat post
for them.
For deeper or more current detail, fetch those posts or search the RevenueCat blog (selling and
acquisition content) and the State of Subscription Apps report. To benchmark the user's app
against its peers, use `get-benchmarks`.
revenuecat-audiences16.1 KB
---
name: revenuecat-audiences
description: >
Use before sharing a link to a filtered customer list or audience, and when identifying,
filtering, or ranking the user's customers (a segment, who they are, a product or duration,
spend, renewals, status, country, attribution).
---
# Audience filters and dashboard links
Use Audiences to filter the user's customers and to share a dashboard link to that set. The same
filters answer "who" questions — they are not a full leaderboard sort.
There is no first-class CLI command for audiences. Do not tell the user RevenueCat cannot filter
or segment customers.
## Identifying, filtering, or ranking customers
Audiences filter on the [fields](#fields) below, including:
- Product and duration — `latestProduct`, `allPurchasedProductIds`, `latestPurchasedOffering`,
entitlements, offers
- Spend and renewals — `totalSpent`, `totalRenewals` (thresholds, not a sort)
- Subscription state — `status`, trial, auto-renew intent, ownership
- Store, platform, and country — `platform`, `latestStore`, `country`, `storefront`
- Dates — first seen, purchases, renewals, expiration, trial, cancellation
- Attribution and experiments — media source, campaign, ad group, keywords, price experiment
- Identity and custom attributes — app user ID, email, locale, `customAttribute:{key}`
Map the question onto those fields, filter, then read a sample.
1. Fetch project-specific values with `get-audience-filter-options` for any field marked
project-specific that the question uses (`project_id`, `fields`).
2. Reuse an existing audience from `list-audiences` if one already matches. Otherwise
`create-audience` with the same `groups`/`conditions` rule shape as [the filter rule](#the-filter-rule).
Body is only `name` and `rules`. `create-audience` persists a saved audience, so get explicit
confirmation first.
3. `get-audience` with `expand: ["customer_sample"]`. Sample rows include `total_spent`, status,
and latest product — not every filter field. Rank or name customers only from fields the
sample returned.
4. Number fields like `totalRenewals` are filters, not sample columns. Filter on a high
threshold and report the sample; do not invent a value that was not returned.
5. Say it is a sample of matches, not an exhaustive ranking of every customer. Filtering and a
`customer_sample` cannot prove a global superlative (who renewed or spent the most). After you
try the steps above, say that: you can show high-threshold matches, not name a unique maximum.
6. Share the dashboard link ([constructing a link](#constructing-a-link) or
[linking to a saved audience](#linking-to-a-saved-audience)).
## Constructing a link
Two shapes: a filtered `all-customers` link for ad-hoc exploration, and a saved-audience link
after `create-audience`.
1. Get the project ID from `list-projects`. For dashboard URLs, **strip the `proj` prefix**.
2. Pick fields and operators from the [field tables](#fields). Do not invent field or operator
names.
3. For project-specific fields, fetch valid values with `get-audience-filter-options` first.
4. Assemble the [rule JSON](#the-filter-rule): one group per OR-branch, conditions inside a group
for AND, values encoded per [value formats](#value-formats).
5. Serialize and URL-encode the rule with a short script (see [encoding the rule](#encoding-the-rule))
— do not encode by hand.
6. Append it as the `filters` query param on
`https://app.revenuecat.com/projects/{project_id}/customer-lists/all-customers`.
## URL format
```
https://app.revenuecat.com/projects/{project_id}/customer-lists/all-customers?filters={encoded_rule}
```
- `{project_id}` — short hex ID from `list-projects` with `proj` stripped.
- Filters only work on the `all-customers` list. The unfiltered Audiences home is
`/projects/{project_id}/customer-lists`.
## Linking to a saved audience
Link with `customer_list_id` — the field `create-audience`, `get-audience`, and `list-audiences`
return alongside `id`:
```
https://app.revenuecat.com/projects/{project_id}/customer-lists/{customer_list_id}
```
**An audience's `id` (`aud…`) and its `customer_list_id` (`list…`) are different identifiers.**
The dashboard route only resolves `customer_list_id`; an `aud…` id in that slot renders
"Audience not found".
**Correct:**
```
https://app.revenuecat.com/projects/56965ae1/customer-lists/list7c1f0a2b93
```
**Wrong** (the audience `id` instead of the `customer_list_id`):
```
https://app.revenuecat.com/projects/56965ae1/customer-lists/audf0269cdf3df84dd2
```
Do not add a `filters` param to a saved-audience link — the audience carries its own rules. If
the response has no `customer_list_id`, link to `/customer-lists` and name the audience rather
than guessing an id.
## Offering to save the filtered view
A filtered `all-customers` link is ad-hoc — nothing about it is saved. Say so in one short
sentence when you share one, and offer to save it: "This view isn't saved — want me to save it
as an audience so you can find it later?"
Offer once per conversation. If they accept, call `create-audience` with the same rule you built
for the link, then link to it. Do not offer when the link is already to a saved audience.
## The filter rule
The `filters` value is URL-encoded JSON with this shape:
```json
{
"groups": [
{
"conditions": [{ "field": "platform", "operator": "is", "value": "android" }]
}
]
}
```
- Conditions within a group combine with AND.
- Groups combine with OR.
- `value` is always a JSON string — booleans as `"true"`/`"false"`, numbers as `"42"`, lists as a
comma-separated string, date ranges and relative dates as stringified JSON (see
[value formats](#value-formats)).
**Correct** (compact JSON, whole value URL-encoded):
```
?filters=%7B%22groups%22%3A%5B%7B%22conditions%22%3A%5B%7B%22field%22%3A%22platform%22%2C%22operator%22%3A%22is%22%2C%22value%22%3A%22android%22%7D%5D%7D%5D%7D
```
**Wrong** (raw JSON, spaces, unencoded quotes/braces):
```
?filters={"groups": [{"conditions": [...]}]}
```
## Encoding the rule
```python
import json
from urllib.parse import quote
rule = {
"groups": [
{"conditions": [{"field": "platform", "operator": "is", "value": "android"}]},
]
}
print(quote(json.dumps(rule, separators=(",", ":")), safe=""))
```
## Fields
The Audiences preview table shows only **Customer**, **Subscription Status**,
**Auto-Renewal Status**, **Spent**, and **Latest Purchase**. Other attributes appear in the CSV
from **Export all**, or on a customer's profile.
Use these exact `field` strings. See
[Audiences](https://www.revenuecat.com/docs/dashboard-and-metrics/audiences).
### Text fields
Operators: `is`, `isNot`, `contains`, `doesNotContain`, `isEmpty`, `isNotEmpty`.
| Field | Meaning |
| ------------------------- | --------------------------------- |
| `customerId` | App user ID |
| `originalAppUserId` | Original app user ID |
| `email` | Email |
| `phoneNumber` | Phone number |
| `locale` | Locale |
| `appVersion` | App version |
| `sdkVersion` | SDK version |
| `platformVersion` | Platform (OS) version |
| `projectId` | Project ID (e.g. `proj1ab2c3d4`) |
| `projectName` | Project name |
| `appConfigId` | App ID (e.g. `app1ab2c3d4`) |
| `appConfigName` | App name |
| `idfa` | IDFA |
| `idfv` | IDFV |
| `gpsAdId` | GPS ad ID |
| `latestPurchasedOffering` | Latest purchased offering |
| `latestOffer` | Latest offer identifier |
| `latestEntitlements` | Latest entitlement identifiers |
| `allPurchasedProductIds` | All purchased product identifiers |
### Enum fields
Operators: `is`, `isNot`, `isAnyOf`, `isNotAnyOf`, `isEmpty`, `isNotEmpty`.
| Field | Values |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `platform` | `iOS`, `android`, `web`, `macOS`, `amazon`, `roku`, `tvOS`, `visionOS`, `watchOS` |
| `status` | `active`, `trialing`, `in_grace_period`, `in_billing_retry`, `paused`, `expired`, `incomplete`, `unknown` |
| `latestStore` | `app_store`, `play_store`, `promotional`, `mac_app_store`, `stripe`, `amazon`, `roku`, `rc_billing`, `paddle`, `external` |
| `anyActiveStore` | Any active store — same store identifiers as `latestStore` |
| `latestOwnershipType` | `PURCHASED`, `FAMILY_SHARED` |
| `latestOfferType` | `no_offer`, `free_trial`, `introductory_offer`, `offer_code`, `promotional_offer`, `win_back_offer`, `unspecified_offer` |
| `priceExperimentVariant` | `a`, `b`, `c`, `d` |
| `country` | Last seen country — ISO 3166-1 alpha-2 codes (e.g. `US`, `DE`) |
| `latestStoreCountry` | ISO 3166-1 alpha-2 country codes |
| `storefront` | Store country — ISO 3166-1 alpha-2 codes |
| `mediaSource` | project-specific — [fetch valid values](#fetching-project-specific-values) |
| `campaign` | project-specific |
| `adGroup` | project-specific |
| `ad` | project-specific |
| `keyword` | project-specific |
| `creative` | project-specific |
| `priceExperimentId` | project-specific |
| `latestProduct` | product IDs of the project — fetch valid values |
### Boolean fields
Operators: `is`, `isNot`. Value is exactly `"true"` or `"false"`.
| Field | Meaning |
| -------------------------------- | ---------------------------------------------------- |
| `hasMadeSandboxPurchase` | Has made a sandbox purchase |
| `hasMadeNonSubscriptionPurchase` | Has made a non-subscription purchase |
| `latestAutoRenewIntent` | Auto-renewal status (`true` = set to renew) |
| `isCurrentlyTrialing` | Currently trialing |
| `isRcPromo` | Has been granted an entitlement via RC (promotional) |
### Number fields
Operators: `equal`, `notEqual`, `greaterThan`, `greaterThanOrEqual`, `lessThan`,
`lessThanOrEqual`, `isEmpty`, `isNotEmpty`. Value is a numeric string, e.g. `"50"`.
| Field | Meaning |
| --------------- | ------------------------ |
| `totalSpent` | Total spent |
| `totalRenewals` | Total number of renewals |
### Date fields
Operators: `before`, `beforeOrOn`, `on`, `after`, `afterOrOn`, `within`, `between`, `notBetween`,
`isEmpty`, `isNotEmpty`.
| Field | Meaning |
| ---------------------- | ------------------------------ |
| `firstSeenAt` | First seen |
| `lastSeenAt` | Last seen |
| `firstPurchaseAt` | First purchase |
| `mostRecentPurchaseAt` | Most recent purchase |
| `mostRecentRenewalAt` | Most recent renewal |
| `latestExpirationAt` | Latest expiration |
| `trialStartAt` | Trial start |
| `trialEndAt` | Trial end |
| `subscriptionOptOutAt` | Most recent cancellation |
| `trialOptOutAt` | Most recent trial cancellation |
### Custom attribute fields
Filter with `customAttribute:{key}` (e.g. `customAttribute:favorite_team`). They use the enum
operators. Fetch known keys and values with `get-audience-filter-options` — never invent a key.
## Value formats
- `isEmpty` / `isNotEmpty` — set `"value": ""` (the value is ignored).
- `isAnyOf` / `isNotAnyOf` — comma-separated string: `"value": "US,CA,MX"`.
- `before`, `beforeOrOn`, `on`, `after`, `afterOrOn` — calendar date `"value": "2026-01-31"`
(`YYYY-MM-DD`).
- `between` / `notBetween` — stringified JSON with exactly `from` and `to`:
`"value": "{\"from\":\"2026-01-01\",\"to\":\"2026-01-31\"}"` (`from` ≤ `to`).
- `within` — stringified JSON with exactly `direction`, `value`, `unit`:
`"value": "{\"direction\":\"last\",\"value\":30,\"unit\":\"days\"}"`. `direction` is `last` or
`next`; `unit` is `minutes`, `hours`, or `days`; `value` is a non-negative integer.
`before`/`beforeOrOn`/`after`/`afterOrOn` also accept this relative format (not `on`).
## Fetching project-specific values
Fields marked project-specific (`mediaSource`, `campaign`, `adGroup`, `ad`, `keyword`, `creative`,
`priceExperimentId`, `latestProduct`) and custom attributes only match values that exist in the
project's data. Fetch with `get-audience-filter-options`:
- `project_id` (required)
- `fields` (required, at least one) — any of the eight fields above, `customAttribute:{key}` for
one custom attribute, or `customAttribute` to list every custom attribute key with its values.
A custom-attribute entry may come back with `cardinality_exceeded: true` — a value the user
stated verbatim can still be valid even if it is not in the list.
Fixed-value fields (`country`, `platform`, `status`, …) are not served by this tool — use the
tables above. Never guess project-specific values — a filter on a non-existent value silently
matches zero customers.
## Example: building a link
User wants: "Android customers acquired through Instagram" in project `proj56965ae1`.
Rule (both conditions in one group — AND):
```json
{
"groups": [
{
"conditions": [
{ "field": "platform", "operator": "is", "value": "android" },
{ "field": "mediaSource", "operator": "is", "value": "Instagram" }
]
}
]
}
```
Link:
```
https://app.revenuecat.com/projects/56965ae1/customer-lists/all-customers?filters=%7B%22groups%22%3A%5B%7B%22conditions%22%3A%5B%7B%22field%22%3A%22platform%22%2C%22operator%22%3A%22is%22%2C%22value%22%3A%22android%22%7D%2C%7B%22field%22%3A%22mediaSource%22%2C%22operator%22%3A%22is%22%2C%22value%22%3A%22Instagram%22%7D%5D%7D%5D%7D
```
User wants: "customers on iOS or Android who are currently trialing and were first seen in the
last 30 days".
```json
{
"groups": [
{
"conditions": [
{ "field": "platform", "operator": "isAnyOf", "value": "iOS,android" },
{ "field": "isCurrentlyTrialing", "operator": "is", "value": "true" },
{
"field": "firstSeenAt",
"operator": "within",
"value": "{\"direction\":\"last\",\"value\":30,\"unit\":\"days\"}"
}
]
}
]
}
```
revenuecat-billing2.98 KB
--- name: revenuecat-billing description: Use when the user asks about their RevenueCat plan, billing, their RevenueCat invoices, or how much they will have to pay RevenueCat. --- # RevenueCat account billing This is the **developer's RevenueCat plan and invoices**, not customer subscription invoices (`rc invoices` / customer billing in the dashboard). Use: - `get-account-billing` — current plan, usage / Monthly Tracked Revenue (MTR), whether a payment method is set up. - `list-account-billing-invoices` — RevenueCat invoices including amount and status. There is no first-class CLI command for account billing. Do not use `rc invoices`, which lists a **customer's** invoices. ## Context - RevenueCat bills based on **Monthly Tracked Revenue (MTR)** for each plan (except `enterprise` plans, which have custom billing). MTR is not Monthly Recurring Revenue (MRR): it includes revenue from all purchases and renewals, including non-subscription products. Current MTR comes from `get-account-billing`. Billing periods are not calendar months — they run from the same date of a month until the same date of the next month (for example July 15 until August 15). - If the user's role on the current project is not `owner`, `get-account-billing` does **not** apply to that project. Only the owner can see what plan the project is under. Tell them to contact the project owner. Use `list-collaborators` (and `list-projects`) to find the owner's email when available. - For users on an `enterprise` plan, direct them to their Customer Success manager for detailed billing. - Billing settings: https://app.revenuecat.com/settings/billing ## Pro plan Most users are on the Pro plan. Pro has a free allowance of $2,500 MTR. If that threshold is crossed in a month, the user is billed 1% of all tracked revenue (invoice amounts are full dollars, rounded down), including the portion below $2,500. If MTR falls back below the threshold in later months, they are not charged. A charge is made only in months where MTR is above $2,500. When that limit is passed and the RevenueCat account is at least one month old, access to some features is restricted immediately until a payment succeeds: - View and Filter Charts - Create new Customer Lists - Export Customer Lists - View Customer History (Customer Details remain) - View individual events - Add Customer Attributes - Create new Experiments - Edit running Experiments (viewing results and stopping remain) - Create new Paywalls - Edit existing Paywalls (using existing Paywalls remains) ## Invoice details The user can update company name, address, and Tax ID/VAT on invoices in [billing settings](https://app.revenuecat.com/settings/billing) under **Invoice details**. If they have not added a payment method, they will not receive invoices and cannot change invoice details. If they need more than one Tax ID, or want invoices forwarded to a different email, they need to contact RevenueCat support. Do not offer this proactively — only when they explicitly ask.
revenuecat-charts22.6 KB
---
name: revenuecat-charts
description:
Use when the user asks about RevenueCat data, analytics, charts, or KPIs — querying charts with
get-chart-options-schema and get-chart-data, interpreting subscription metrics, or sharing
dashboard chart links. For forecasts, projections, or run-rates, use revenuecat-forecasting.
---
# Accessing RevenueCat charts
When querying a RevenueCat chart, follow this workflow:
1. Use `get-chart-options-schema` to discover a chart's available options.
2. Use `get-chart-data` with the right options to retrieve the chart data.
3. Analyze the data, using scripts for any non-trivial arithmetic.
Via the `rc` CLI (see the `revenuecat-cli` skill): `rc charts list` to list charts, `rc charts options <chart>` for the schema, and `rc charts show <chart>` for the data.
In general, to avoid clogging the context, start with defined timeframes and larger resolution, then narrow down.
## 1. Discover chart options with `get-chart-options-schema`
- Treat `get-chart-options-schema` as the source of truth for each chart before calling
`get-chart-data`. It returns the chart's supported `resolutions`, `filters`, `segments`, and
`user_selectors`. Always call this tool with `"realtime": true`. Later `get-chart-data` calls must
use string IDs exactly as returned here.
- `filters` are the dimensions you may later constrain in `get-chart-data`.
- Each filter has:
- an `id` to later use as the filter `name`.
- a `value_mode` that tells you how to choose valid values:
- `inline_enum` means you must use the `id` of one of the returned `options`. Resolve
user-supplied names first with the matching list tool, such as `list-products`,
`list-offerings`, `list-apps`, etc.
- `inferred_standard` means use the standard code from `value_source` such as an ISO country
code.
- `dynamic` means values come from observed project data and must match exactly.
- Do not pass display names, store product identifiers, bundle IDs, or guessed values unless the
schema says they are valid values.
- `segments` are the dimensions you may later group by in `get-chart-data` using `segment`.
- A segment entry directly gives the dimension `id` to use. It does not list segment values
because the chart will group by it and show all values in the output.
- Filters and segments are separate per-chart lists, so never assume a filterable dimension is
segmentable. For example, `conversion_to_paying` may support `product_id` and
`offering_identifier` as filters but not as segments.
- `user_selectors` are chart-specific switches that change what metric or window the chart returns.
Each selector is keyed by the selector ID to pass in `get-chart-data`'s `selectors` JSON object
and usually includes allowed option IDs plus a default. For example, the `revenue` chart may use
`revenue_type` (`revenue`, `revenue_net_of_taxes`, `proceeds`), while conversion charts may use
`conversion_timeframe` and default to `7_days`. State non-default selector choices when presenting
results.
- `resolutions` list the supported time granularity and their string IDs for `get-chart-data`. You
must always pass one of these resolution IDs (such as `"0"` for day or `"2"` for month) when later
calling `get-chart-data`.
## 2. Retrieve chart data with `get-chart-data`
### Calling `get-chart-data`
- Always set `"realtime": true` and specify start date, end date and resolution ID.
- Always follow the guidelines from a prior `get-chart-options-schema` for that chart.
- Consider rate limits: don't query too many charts at once.
- Date ranges are inclusive (start_date and end_date are included in the range). When asked for
data for the "last N days", take that into account (use today as end date, start date is (N-1)
days before today).
- Use available `filters` to constrain the output. They are a JSON-encoded array of
`{"name": "<filter id>", "values": ["<value id>", ...]}`.
- Values within one entry are ORed; separate entries are ANDed. Example: App Store revenue in the
US or the UK:
`"[{\"name\": \"store\", \"values\": [\"app_store\"]}, {\"name\": \"country\", \"values\": [\"US\", \"GB\"]}]"`.
- Use at most one entry per filter name: a repeated name silently replaces the earlier entry
(it does not combine with it). Filter values must not contain commas.
- Use the available `selectors` for configuring the chart. They are a JSON-encoded object mapping
selector IDs to option IDs, e.g. `"{\"revenue_type\": \"proceeds\"}"`. Omitted selectors use their
defaults; the response echoes the applied values in `user_selectors`.
- Use `segment` to group the output by some of the segmentable dimension IDs:
- Note that segmenting multiplies output size. You can keep responses small by using a coarser
resolution, a shorter date range, `limit_num_segments` (keeps the top N by value and folds the
rest into "Other"), or `aggregate` when you only need per-segment totals.
- Use `aggregate` for summary-only questions such as totals or averages (e.g. "total Q1 revenue").
Prefer this over fetching and computing from raw data points yourself. Combined with `segment` it
returns compact per-segment summaries (e.g. country averages). In the output, `values` will be
empty and `summary` will contain just those operations.
- Pass `currency` to convert outputs to some monetary unit (see `yaxis_currency` in the response).
### Reading `get-chart-data` outputs
- `measures` lists the metrics the chart returns (display name, unit, description). Most charts
return several, e.g. `revenue` may return Revenue, Transactions, and Ad Impressions.
- `values` is a flat array of points `{cohort, measure, value, incomplete}`, plus `segment` when
segmented. `cohort` is the Unix timestamp of the period start; `measure` and `segment` are indexes
into the `measures` and `segments` arrays. The first segment is usually a `"is_total": true` -
never sum it together with the other segments.
- `summary` holds `total` and `average` per measure display name, nested per segment when segmented.
- Points with `incomplete: true` cover partial periods: the current period, and the first period
when `start_date` falls mid-period (since `expand_periods` defaults to false). Exclude them from
trend or comparison analysis, and call them out when presenting. Point-in-time charts (MRR,
actives, trials) ignore `expand_periods`: their values are snapshots at period boundaries and are
never partial.
- `annotations` lists dated notes the user made on their dashboard (e.g. releases, launches or
experiments). Check them when explaining movements in the data.
- Invalid filters, segments, or selector values fail with a 400 `parameter_error` whose message
lists the supported IDs. On such errors, re-read the options schema instead of retrying guesses.
## 3. Analyze the data
- Segmented responses include a `Total` segment, and the `limit_num_segments` cap folds segments
beyond the top N into an `Other` segment. Use `Total` as the baseline; do not sum segments
yourself.
- Do complex arithmetic on chart output (growth rates, segment shares, combining numbers across
calls) with scripts (e.g. `jq` or a short Python script) instead of reasoning over the numbers.
- The most recent period may be flagged incomplete. Do not compare it against full periods without
saying so.
- Before speculating about the cause of a metric shift, first check the available user annotations.
- Cohort charts measure within a cumulative window from first seen, chosen by a selector
(`conversion_timeframe` on conversion charts, `customer_lifetime` on realized LTV charts), one
window per call. State the window when presenting results and hold it constant when comparing
cohorts.
# Interpreting metrics
Subscription apps are driven by four forces:
- Acquisition - how many new customers are arriving to the app
- Conversion - how many of those customers are converting into trials or paid plans
- Retention - how long do those customers retain
- Reactivation - how can you bring back old users
The net movement of an apps revenue will be the result of the combination of these forces. When
giving advice, always use benchmark data to make sure you aren't incorrectly diagnosing an issue.
General guidelines:
- Before telling the user RevenueCat has no source for a metric they named, pick the likely
chart(s), call `get-chart-options-schema` for options, then `get-chart-data` and check its
`periods` / `measures` — unfamiliar names are often one period or measure inside a chart
(schema alone does not list those). Missing from `get-benchmarks` means no peer percentile
band, not that the value can't be computed.
- After looking: if nothing in the tools matches, or two readings would produce materially
different numbers, ask the user to define the metric. Do not invent a definition.
- When using the data tools, date ranges are inclusive (start_date and end_date are included in the range). When asked for data for the "last N days", take that into account (use today as end date, start date is (N-1) days before today).
- Provide links to RevenueCat charts (see the Dashboard URL Format section below) where it is useful. Provide specific links including filters, segments, date ranges, etc — eg. if you are asked for proceeds in the last 3 months, link to the revenue chart with custom date range of the last 3 months and the `revenue_type` selector set to `proceeds`, don't link to the plain revenue chart
- For forecasts, projections, or run-rates, load the `revenuecat-forecasting` skill before pulling charts.
## Revenue
- When asked for general revenue numbers without additional specification, default to gross revenue (ie. revenue including taxes and store commissions) and call it out.
## Acquisition
- Use the New Customers chart to understand how much top of funnel the app is driving.
- Segmenting New Customers by Country, or Apple Ads dimensions can be helpful in informing
acquisition.
- RevenueCat's Apple Ads integration sets attribution dimension information like campaign, ad
group, keyword
- Developers can also manually set these attribution dimensions on a per-customer level using
reserved customer attributes
- Do not treat a zero result from an explicit attribution filter as proof that the broader channel
has zero users or zero activity. For example, `attribution_source = Organic` only means users
explicitly tagged with that value; it does not include untagged users or every organic/non-paid
user.
- If attribution data is sparse or missing, say that clearly. Use "unattributed" or "not explicitly
tagged" rather than assuming those users came from a specific channel.
## Conversion
The definition of conversion may vary depending on what model the app is using. They may be
converting to a trial, that then converts into a subscription. Or they may be sending users directly
to a subscription.
- Use the Initial Conversion chart to see the proportion of new customers that start a subscription
or trial within the selected conversion timeframe.
- Use the Conversion to Paying chart to see the proportion of new customers that made a payment
within the selected conversion timeframe.
- Initial Conversion (started a trial or subscription) and Conversion to Paying (made a payment)
measure different events. Never use one as a stand-in for the other, or compare a value from one
against a value from the other.
- You can then further determine if they are using free trials by looking at the New Trials chart.
- The Trial Conversion Rate chart is a helpful chart for understanding the performance of just that
trial conversion.
- Filtered charts keep the all-new-customers denominator. For example, filtering Conversion to
Paying on a specific `product_id` gives the share of ALL new customers converting to that product,
not that product's own conversion rate. State this caveat when presenting filtered results.
## Retention
- The Churn chart will tell you the % of the active subscriber base that is lost each period. It can
be difficult to interpret or benchmark because it is a blend of different periods.
- When you want to understand the long term retention of different products, look at the
Subscription Retention chart or the Cohort Explorer chart using the `retained_subscriptions`
measure, which returns how many subscriptions remained active (ie. not expired) over time.
- To understand when in their lifecycle subscriptions get cancelled (ie. auto-renewal turned off),
use Cohort Explorer with the `subscriptions_set_to_renew` measure.
- The Subscription Retention chart reports each cohort's renewals period by period, as counts and
precomputed rates ("Month N" / "Month N rate" columns). A single period answers questions like
"what share renewed once": the first renewal is the period matching the plan length (Month 1 for
monthly plans, Year 1 for annual). Filter by `product_duration` to keep one plan length per read,
and by `subscription_type` (`new`) to exclude product changes and resubscriptions. Periods a
cohort hasn't had the full opportunity to reach are reported as incomplete — don't read them as
zeros.
## Reactivation
- The only real way to understand Reactivation is looking at the MRR Movement chart and the
Resubscription MRR
## Investigating metric shifts
When a metric change needs explaining — revenue dropped, trials fell, conversion spiked — follow
this order before answering:
1. **Quantify the shift.** Pull the chart data, confirm the magnitude and timing.
2. **Check configuration.** Offerings, packages, products, paywalls and experiments are not visible
in metrics, so never infer them from a chart. If your answer names any of them, look it up in
this run:
- `list-experiments` with `status="stopped"` and `status="running"`. If an experiment stopped
near the shift, call `get-experiment-results` to see which variant won.
- `list-offerings` with `limit: 100` (the default page of 20 rarely covers a real project), then
`get-offering` on the `is_current` id with `expand: ["package.product"]`. An offering with
`paywall_id: null` has no RevenueCat paywall — load `revenuecat-paywall-design` before giving
paywall advice.
- `get-product-store-state` before saying a product is retired, unavailable, or no longer
selling. Report store status in plain language, never raw field names.
- If experiments and offerings don't explain it, `list-paywalls` for paywall changes.
3. **Check annotations.** Look at the `annotations` field in the chart response.
4. **Only then form a hypothesis.** Present it as a hypothesis, not a finding. An unverified guess
about configuration is a missing tool call, never your headline finding.
Do not skip step 2. Once you have made the calls, if their results cannot explain the shift, say
so explicitly rather than constructing a mechanism.
## Populations and denominators
A rate only describes the population in its denominator. Before presenting one, check that this is
the population the question is about.
- When one segment dominates the denominator, the blended rate describes that segment, not the app.
Re-query filtered to the population the question is about and lead with that number.
- **Never present a rate as evidence while also calling its denominator inflated or
unrepresentative.** Re-query with a filter instead of caveating.
- Report the filtered numbers yourself rather than recommending the user go look at a filtered
chart.
## Analytics comparisons
- Compare like with like. Any two numbers compared against each other must come from the same
chart and metric, with the same conversion window and cohort definition. Use the same date range
too, except in deliberate period-over-period comparisons.
- If you have a metric for one side of a comparison but not the other, query the missing side with
the same chart and settings before comparing. Do not substitute a value from a different chart.
- For open-ended questions like "how are {segment} users doing?", do not stop at segment-only
metrics. Pull the requested segment and an overall/unfiltered baseline for the key conversion or
revenue-quality metric, then judge performance relative to that baseline. Do not evaluate a
segment as "healthy", "underperforming" etc. without comparing it to a baseline.
- Do not compare revenue or conversions from a filtered new-customer cohort against total app
revenue from all cohorts and renewals. If you cannot get a matching baseline, say so and avoid
directional performance claims.
- When a user is confused that two metrics diverge, say what each one counts before explaining the
gap.
Wrong — different charts merged under one header:
| Country | Conversion to paying (14d) |
| ------- | ----------------------------------- |
| US | 26.3% (this is Initial Conversion) |
| PL | 2.1% (this is Conversion to Paying) |
Correct — one column per metric, every value in a column from the same chart, metric, and settings:
| Country | Initial conversion (14d) | Conversion to paying (14d) |
| ------- | ------------------------ | -------------------------- |
| US | 26.3% | 9.6% |
| PL | 4.3% | 2.1% |
# Chart Dashboard Links
Generate shareable links to RevenueCat dashboard charts.
## Constructing a Link
A chart link must follow a specific [Dashboard URL Format](#dashboard-url-format) and must be built
from a verified previous successful `get-chart-data` call.
0. If there isn't a previous successful `get-chart-data` call for this chart, follow the Querying
RevenueCat charts workflow above first.
1. Construct the link, starting with base:
`https://app.revenuecat.com/projects/{project_id}/charts/{chart_name}`.
2. Add [`range` param](#range-param--required) with date range. This is required.
3. Add [`resolution` param](#resolution-param) with resolution. Don't trust defaults.
4. Add any filters as [`filter` params](#filter-params).
5. Add segment as [`segment` param](#segment-param), if segmenting.
6. Add [chart-specific selectors](#chart-specific-selectors) as needed.
7. URL-encode all values (spaces → `+`, colons → `%3A`, etc.)
## Dashboard URL Format
**IMPORTANT**: Use this exact structure:
```
https://app.revenuecat.com/projects/{project_id}/charts/{chart_name}?range={range_value}
```
- `{project_id}` — The short hex ID (e.g., `56965ae1`), not the full `proj56965ae1`
- `{chart_name}` — The same chart name used with `get-chart-data` (`revenue`, `churn`, `mrr`,
`conversion_to_paying`, etc.)
- Project ID goes in the **path**, not as a query parameter
**Correct example:**
```
https://app.revenuecat.com/projects/56965ae1/charts/revenue?range=Custom%3A2025-11-16%3A2026-02-13
```
**WRONG — do not use:**
```
https://app.revenuecat.com/charts/revenue?project=proj56965ae1&chart_start=...&chart_end=...
```
## Query Parameters
### `range` param — required
The `range` parameter controls the date range. Format: `{preset}:{start_date}:{end_date}`, with
start_date and end_date in YYYY-MM-DD format. Use `Custom` as the preset.
**Always use this format** — do not use `start_date`, `end_date`, `chart_start`, or `chart_end`
params. Note: The `:` between parts must be URL-encoded as `%3A`.
Example: `range=Custom%3A2025-01-01%3A2025-12-31`
### `resolution` param
| Value | Meaning |
| ----- | --------------------- |
| `0` | Daily granularity |
| `1` | Weekly granularity |
| `2` | Monthly granularity |
| `3` | Quarterly granularity |
| `4` | Yearly granularity |
### `segment` param
Dimension to break down the data by. Use the exact dimension ID you were using to make the
`get-chart-data` request.
- `country` — by country
- `store` — by app store (App Store, Play Store, etc.)
- `product_id` — by product identifier
- `platform` — by platform (iOS, Android, etc.)
- `offering_identifier` — by offering
Segments vary per chart — only link a segment you successfully used in a `get-chart-data` call for
that chart.
### `filter` params
Filters are passed as individual query `filter` params with the content
`{dimension}%3A%3D%3A{value}`. Use the dimension names you used for the `get-chart-data` request.
| Dimension | Example |
| ------------ | ------------------------------------------ |
| `country` | `filter=country%3A%3D%3AUS` |
| `store` | `filter=store%3A%3D%3Aapp_store` |
| `product_id` | `filter=product_id%3A%3D%3Aprodbb68905d98` |
| `platform` | `filter=platform%3A%3D%3AiOS` |
To use multiple filters, regardless of whether they are for the same dimension or multiple
dimensions, include multiple `filter` query parameters. Passing multiple filters for the same
dimension will result in an OR operation, passing filters for different dimensions will result in an
AND operation.
### Chart-Specific Selectors
Selectors are passed as individual query params, with the same names and values used in the
`get-chart-data` `selectors` argument. Orientative examples (truth in `get-chart-data`):
- `revenue_type` (revenue chart) — `revenue`, `revenue_net_of_taxes`, or `proceeds`
- `conversion_timeframe` (conversion charts) — `0_days`, `3_days`, `7_days`, `14_days`, `30_days`,
or `unbounded`
- `customer_lifetime` (realized LTV charts) — `7_days`, `14_days`, `30_days`, `3_months` up to
`24_months`, or `unbounded`
## API to Dashboard Parameter Mapping
When translating from API parameters to dashboard URLs:
| API Parameter | Dashboard Parameter |
| ------------------------- | ------------------------------------------------------ |
| `start_date` + `end_date` | `range=Custom%3A{start}%3A{end}` (use `Custom` preset) |
| `segment` | `segment` |
| `filters` (JSON array) | Individual `filter` query params |
| `selectors` (JSON object) | Individual query params |
## Example: Building a Link
User wants: "Revenue chart for last 90 days, segmented by country, filtered to US and Germany"
Calculate dates: if today is 2026-02-13, then 90 days ago is 2025-11-16.
```
https://app.revenuecat.com/projects/56965ae1/charts/revenue?range=Custom%3A2025-11-16%3A2026-02-13&segment=country&filter=country%3A%3D%3AUS&filter=country%3A%3D%3ADE
```
User wants: "Churn chart from August 2025 to now"
```
https://app.revenuecat.com/projects/56965ae1/charts/churn?range=Custom%3A2025-08-01%3A2026-02-13
```
## Getting Project ID
The project ID can be found via the `list-projects` tool, which lists all projects with their ID.
- The tool returns IDs starting with `proj`, for example `proj56965ae1`
- **For dashboard URLs, strip the `proj` prefix** — use just `56965ae1` in the path
revenuecat-cli3.58 KB
---
name: revenuecat-cli
description: Drive RevenueCat from the terminal with the `rc` CLI, an alternative to the RevenueCat MCP server for humans, CI, and agents. Covers install, authentication, command discovery, and output conventions. Referenced by the other RevenueCat skills whenever they offer a CLI path.
---
# revenuecat-cli: driving RevenueCat from the terminal
`rc` is the official RevenueCat command line interface. It covers most of the same project, app, product, entitlement, offering, paywall, chart, and store-state operations as the RevenueCat MCP server (the CLI adds paywall generate/edit; the MCP server has some SDK feature-gate and experiment tools the CLI does not). Use whichever surface is available; this skill is the reference for the CLI path.
## Install and authenticate
- Run without installing: `npx @revenuecat/cli <command>` (good for CI and agent sandboxes).
- Or install: `brew install RevenueCat/tap/rc`, or `npm install -g @revenuecat/cli`.
- Authenticate once with `rc auth login` (browser OAuth), or set `RC_API_KEY`. Non-interactively, pass `--api-key` or set `RC_API_KEY`.
- The CLI authenticates with a RevenueCat **secret** key (`sk_...`); that is correct because the CLI is a server-side tool. This is not the same as the **public** SDK keys (`appl_`/`goog_`/`amzn_`) you fetch for client app code. Never put a secret key in an app.
## Discover the surface
The CLI is self-describing. Don't guess a command or flag, ask the binary:
- `rc commands --json` lists the full command tree. Capability IDs appear in colon form (`apps:create`, `products:store:plan`).
- `rc commands --schemas --json` returns every command's flags, args, and examples in one call. This is the most reliable way for an agent to discover per-command detail.
- `rc schema <command> --json` returns detail for a single command, but the command must be **space-separated tokens** (`rc schema apps create`), not the colon form from `rc commands`. `rc schema apps:create` silently returns the root schema, so when in doubt use `rc commands --schemas`.
- `rc --help` and `rc <noun> --help` give human-readable help.
## Output conventions
- `--json` gives stable, machine-readable output. Data goes to stdout, progress and chatter to stderr.
- The envelope is `{ "data": ... }`, but the shape under `data` varies by command: lists are usually `data.items[]`, though some commands use other keys (charts use `data.names[]`). Inspect the schema or the response rather than assuming `data.items`.
- `--no-input` never prompts, it fails instead. Use it in scripts, CI, and agents.
- `--yes` / `-y` skips confirmation prompts on mutating commands.
- `--project-id <id>` selects the project (or set a default with `rc projects use`). Pass it explicitly in scripts.
- Exit codes: `0` success, `1` error, `2` bad usage, `4` auth, `5` not found, `6` rate limited.
## Common operations
Look up exact flags with `rc commands --schemas --json` (or `rc schema <space-separated command>`). The common nouns:
- **Projects and apps**: `rc projects list|create|use`, `rc apps list|create|keys`
- **Catalog**: `rc products ...`, `rc entitlements ...`, `rc offerings ...`, `rc packages ...`
- **Store credentials and state**: `rc setup apple|google`, `rc products store plan|apply|sync`
- **Data**: `rc charts list|show`, `rc customers ...`
- **Paywalls**: `rc paywalls generate|edit|publish` (dashboard design/audit playbook: `revenuecat-paywall-design`)
- **Dashboard**: `rc open [section] [id] --print` (resource URLs: `revenuecat-dashboard-links`)
Prefer the specific command. Drop to `rc api <METHOD> <path>` only for endpoints not yet in the CLI surface.
revenuecat-customer-center5.39 KB
---
name: revenuecat-customer-center
description: "Add the RevenueCat Customer Center (self service subscription management UI) to an app. Use when the user asks to add a customer center, build a self service subscriptions screen, let users manage subscriptions in app, add a subscription management screen, present CustomerCenterView, call presentCustomerCenter, or wire a 'manage subscription' button to the RevenueCat customer center on iOS, Android, Kotlin Multiplatform, Flutter, or React Native."
---
# revenuecat-customer-center: add the RevenueCat Customer Center
Use this skill when the user wants an out of the box UI that lets their customers manage active subscriptions, request refunds, cancel, restore, or contact support, without shipping custom UI. The UI is configured in the RevenueCat dashboard and rendered by the `RevenueCatUI` SDKs.
Prerequisite: `integrate-revenuecat` has already run. `Purchases.configure(…)` must succeed before the Customer Center can load customer data.
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency. The Customer Center ships in `react-native-purchases-ui`. Read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root. The Customer Center ships in `purchases_ui_flutter`. Read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` has a `kotlin { … }` multiplatform block, or depends on `com.revenuecat.purchases:purchases-kmp*`. The Customer Center composable is in `purchases-kmp-ui`. Read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP). The Customer Center composable is in `com.revenuecat.purchases:purchases-ui`. Read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root. `CustomerCenterView` is in `RevenueCatUI`. Read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
## 2. Shared concepts (all platforms)
- **Customer Center is a dashboard configured UI.** Actions, copy, promotional offers, refund flows, cancel survey options, and support contact details are defined in the RevenueCat dashboard under **Customer Center**. Without configuration, the view renders a minimal default layout; users will see almost nothing useful.
- **It needs an identified user with purchases** to surface anything meaningful. If the user is anonymous and has never bought anything, the Customer Center renders an empty / restore only state. If the app supports login, call `Purchases.logIn(userId)` before opening the Customer Center.
- **Customer Center is separate from paywalls.** The standard pattern: expose a "Manage subscription" row in the settings screen that opens the Customer Center. Paywalls are for new purchases; the Customer Center is for existing subscribers.
- **The UI owns the flow.** Restore, cancel, refund, and promotional offer flows run inside the component. Listen for the lifecycle callbacks (`onRestoreCompleted`, `onRefundRequestStarted`, `onManagementOptionSelected`, `onPromotionalOfferSucceeded`, etc.) to react in app code, not to drive the flow.
- **Platform availability varies.** iOS has had Customer Center longest; Android, KMP, Flutter, and React Native follow. Platform files flag any gaps. Refund requests are an iOS only action because only Apple exposes in app refund requests; on Android, the "Manage subscription" option links out to the Google Play subscriptions screen.
- **If the installed SDK version is older than Customer Center support**, the fallback is a manual subscription management screen: show the user's active entitlements from `Purchases.customerInfo()`, expose a `Purchases.restorePurchases()` button, and link to the store's subscription management URL. Point the user to upgrade the SDK if they want the full Customer Center.
## 3. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
Each platform file is self contained: install command, exact snippet to present the Customer Center, and the callback shape.
## 4. Verify
Do not claim the integration is complete until:
1. The project **builds** on the target platform.
2. Sign into the app with a test user that has **at least one active sandbox subscription**. Trigger the code path that opens the Customer Center. The UI loads and the subscription appears in the list.
3. At least one configured action runs end to end. The simplest check: tap **Restore purchases** and confirm the restore completed callback fires with a non-empty `customerInfo.entitlements.active` map. If the dashboard has Cancel / Refund / Support actions configured, verify the corresponding callback (`onManagementOptionSelected`, `onRefundRequestStarted`, etc.) fires when the user taps through.
4. Dismissing the Customer Center fires the `onDismiss` callback.
If the Customer Center opens but is empty, the signed in user has no purchases, or the dashboard Customer Center section is not configured. Fix in the dashboard, reload, and retry.
Referenced files: 5
revenuecat-dashboard-links3.62 KB
---
name: revenuecat-dashboard-links
description:
How to link to pages in the RevenueCat dashboard (offerings, products, customers, settings,
integrations, paywalls, experiments, etc.). For chart links with filters, use revenuecat-charts;
for filtered customer lists, use revenuecat-audiences.
---
# Dashboard links
Share `https://app.revenuecat.com/...` links so the user can open the page you are talking about.
Do not invent IDs. If you do not have one, ask or omit the link.
Via the `rc` CLI (see the `revenuecat-cli` skill): `rc open [section] [id] --print` prints a
deep link for a coarse section (`paywalls`, `customers`, `experiments`, `charts`, `apps`,
`overview`, `settings`, `integrations`, `catalog`, `api-keys`, `audit-logs`). Use the routes
below when you need a specific resource.
For **chart/metric links with filters and date ranges**, use `revenuecat-charts`. For **filtered
customer lists and audiences**, use `revenuecat-audiences`.
## URL rules
- Base: `https://app.revenuecat.com`
- `{projectId}` in these paths is the **short hex ID** (e.g. `56965ae1`), not `proj56965ae1`.
`list-projects` returns IDs starting with `proj` — strip that prefix for dashboard URLs.
- Fill every `{param}` with a real ID from an API/MCP response.
- Do not add query parameters unless `revenuecat-charts` or `revenuecat-audiences` says so.
- `$listId` on `/customer-lists/{listId}` is an audience's `customer_list_id` (`list…`), not its
`id` (`aud…`).
## Account (no project)
| Path | Page |
| --- | --- |
| `/overview` | Home / account overview |
| `/settings/account` | Account settings |
| `/settings/billing` | RevenueCat plan and billing |
| `/settings/billing/invoices` | Billing invoices |
| `/settings/security` | Security, 2FA |
| `/projects/add` | Create a project |
## Project pages
Prefix every path with `/projects/{projectId}`.
| Path | Page |
| --- | --- |
| `/` or `/overview` | Project overview |
| `/apps` | Apps list |
| `/apps/{appId}` | App details, store credentials |
| `/new-app` | Add an app |
| `/api-keys` | API keys |
| `/sdk-compatibility` | SDK versions and feature compatibility |
| `/settings` | Project settings |
| `/collaborators` | Team members and roles |
| `/audit-logs` | Audit logs |
| `/product-catalog/entitlements` | Entitlements list |
| `/product-catalog/entitlements/{entitlementId}` | Entitlement detail |
| `/product-catalog/products` | Products list |
| `/product-catalog/products/{productId}` | Product detail |
| `/product-catalog/product-editor` | Bulk product editor |
| `/product-catalog/offerings` | Offerings list |
| `/product-catalog/offerings/{offeringId}` | Offering detail |
| `/product-catalog/virtual-currencies` | In-app currencies |
| `/paywalls` | Paywalls list |
| `/paywalls/{paywallId}/builder` | Paywall builder |
| `/targeting` | Targeting rules |
| `/experiments` | Experiments list |
| `/experiments/{experimentId}` | Experiment detail / results |
| `/lifecycle/customer-center` | Customer Center config |
| `/lifecycle/winback` | Win-back campaigns |
| `/lifecycle/retention` | Retention offers |
| `/funnels` | Web funnels |
| `/charts` | Charts home (see `revenuecat-charts` for filtered chart URLs) |
| `/benchmarks` | Benchmarks |
| `/customer-lists` | Audiences / customer lists |
| `/customer-lists/{listId}` | Saved audience (`customer_list_id`) |
| `/customers/{appUserId}` | Customer profile |
| `/integrations` | Integrations home |
| `/integrations/webhooks` | Webhooks |
## Examples
Offering detail:
```
https://app.revenuecat.com/projects/56965ae1/product-catalog/offerings/ofrng_default
```
Paywall builder:
```
https://app.revenuecat.com/projects/56965ae1/paywalls/pw_abc123/builder
```
revenuecat-entitlements-gate3.77 KB
---
name: revenuecat-entitlements-gate
description: "Check whether a RevenueCat user currently has access to a paid feature via entitlements. Use when the user asks to gate a feature behind premium, check if the user has a pro subscription, read customerInfo active entitlements, show or hide a feature based on subscription status, react to entitlement changes, or 'is the user subscribed' on iOS, Android, Kotlin Multiplatform, Flutter, or React Native."
---
# revenuecat-entitlements-gate: check a RevenueCat entitlement
Use this skill when the user wants to decide whether to show or hide a feature based on an active RevenueCat entitlement. The skill covers the one shot check and the reactive listener; it does not cover purchasing (see `revenuecat-purchase-flow`) or auth (`revenuecat-identify-user`).
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency → read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root → read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*` → read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP) → read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root → read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
## 2. Shared concepts (all platforms)
- **Check the entitlement identifier, not the product ID.** The identifier (for example `"premium"`) is configured in the RevenueCat dashboard and mapped to one or more products. Using the entitlement lets you change products, prices, and stores without touching app code.
- **`customerInfo.entitlements.active` is the source of truth.** It is a `Map<String, EntitlementInfo>` keyed by entitlement identifier. Presence in `active` means the user currently has access. Absence means they do not, regardless of past purchases.
- **Do not gate on purchase history.** Expired subscriptions still appear in `customerInfo.entitlements.all` but drop out of `active`. Use `active` only.
- **Fetch once, then subscribe.** The first `customerInfo` call returns a cached value quickly, and the SDK refreshes in the background. Every SDK exposes a listener or stream that fires when entitlements change (after a purchase, restore, renewal, or expiration). Subscribe to that instead of polling.
- **The SDK must be configured first.** If `Purchases.configure(…)` has not run, the entitlement call will fail. Set up the SDK via `integrate-revenuecat` before using this skill.
## 3. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
Each platform file shows the one shot check, the reactive subscription, and where to place each in a typical app.
## 4. Verify
Do not claim the gate works until:
1. A user with an active entitlement sees the gated feature, and a user without it does not.
2. When the entitlement state changes (test with a sandbox purchase or a manual grant in the dashboard), the UI updates without a manual restart, confirming the listener is wired.
3. The entitlement identifier in the code matches an identifier that exists in the RevenueCat dashboard. A typo here silently gates everyone out.
Referenced files: 5
revenuecat-experiments4.84 KB
--- name: revenuecat-experiments description: Use when the user asks to create, prepare, or set up a RevenueCat experiment (A/B test), to prepare its variant offerings or paywalls, or to start, pause, resume, or stop an experiment via the RevenueCat MCP experiment tools --- # Setting up and managing experiments A RevenueCat experiment compares two to four offerings: `offering_a` (the control) against `offering_b`–`offering_d` (the treatments). Enrolled customers are served their variant's offering instead of the project's current offering. Setting up an experiment means preparing one offering per variant, then creating the experiment as a draft. Refer to the MCP tool schemas for the exact parameters of each tool; the `create-experiment` schema also lists the available experiment types with their recommended primary and secondary metrics. ## Preparing the variants Keep the variants identical except for the one aspect under test, otherwise results cannot be attributed to the change. For example, when measuring the impact of a lower subscription price, both variants should share the same paywall design and packages, differing only in the price of the products. **Control.** When the control variant is "what production does today", pass the current offering (`is_current` in `list-offerings`) directly as `offering_a_id` — do not duplicate it. Duplicate only when control itself needs changes relative to production. **Treatments.** First check with `list-offerings` whether an offering already exists that matches what the treatment should serve, and reuse it instead of duplicating. Otherwise, start each treatment from a duplicate of the offering closest to it (usually the control offering). `duplicate-offering` copies the packages (attaching the same existing products) and, if the source offering has a paywall, also copies it as a new unpublished draft; it returns the new offering, including its new `paywall_id`. Then apply the one change under test: - Paywall change (copy, layout, CTA, pricing display, ...): this requires the app to use RevenueCat Paywalls — a paywall is an optional property of an offering, and if the control offering has none (`paywall_id` is `null`). Otherwise, load `revenuecat-paywall-design` and edit the duplicated paywall with `edit-paywall-ai`. Request only the minimum changes required to measure the desired effect, and verify the result by looking at the resulting paywall. - Price, trial, or introductory-offer change: these live on store products, so the treatment offering needs different products. Check with `list-products` whether suitable products already exist; create only the missing ones and their store counterparts (see the `revenuecat-store-state` skill). Then swap them into the duplicated offering's packages: detach the copied products with `detach-products-from-package`, then attach the new ones with `attach-products-to-package`. - Brand-new paywall: duplicate the offering with `include_paywall: false`, then load `revenuecat-paywall-design` and call `create-paywall-ai` with the new offering's `offering_id` so the generated draft is attached to it directly. A paywall and an offering pair 1:1 — `attach-offering-to-paywall` fails if either side is already paired; undo a wrong pairing with `detach-offering-from-paywall` (unpublish the paywall first if it is published). **Publish treatment paywalls.** A duplicated or newly created paywall is an unpublished draft, and a variant only serves the published version. If new paywalls were created in previous steps, publish each treatment paywall with `publish-paywall` before the experiment starts. This is the exception to the rule against proactive publishing, and it is safe: the treatment offering is not the current offering, so no customer sees the paywall until the experiment starts enrolling. ## Creating the experiment Create a draft with `create-experiment`: display name, `enrollment_percentage`, the variant offering IDs, `experiment_type`, a `primary_metric` (plus `secondary_metrics`) matching the hypothesis, `enrollment_mode` (`only_new` unless the user explicitly wants to include existing customers), and any targeting and notes. Creating never starts the experiment. Adjust a draft with `update-experiment`. Before starting, verify each variant: treatment paywalls are published, packages contain the intended products, and any new products are available on the stores. ## Lifecycle - `start-experiment` begins enrolling real customers — only run it with the user's explicit confirmation. - `pause-experiment` stops enrollment but keeps serving enrolled customers their variant and keeps collecting data. - `resume-experiment` continues enrollment. - `stop-experiment` is terminal: enrolled customers fall back to the default offering and the experiment can never be restarted. To interpret results once the experiment is running, use the `revenuecat-experiment-analysis` skill.
revenuecat-forecasting10.9 KB
---
name: revenuecat-forecasting
description:
Use this skill whenever the user asks for a forecast, projection, extrapolation, or run-rate
(MRR, ARR, revenue, or subscribers N months out; revenue to the end of the month or year), or
asks what ad spend or acquisition is needed to hit a growth target. Load it before pulling charts
for the projection.
---
# Forecasting
Use `revenuecat-charts` for chart mechanics and metric interpretation. This skill covers turning
them into a projection. Prefer MCP `get-chart-options-schema` / `get-chart-data` over
`rc charts show` — the CLI surface does not expose expiration-month segmentation and selectors
this playbook needs.
Pick the section that matches the ask:
- **Month-by-month MRR/ARR** (including solve-for-spend, and **"in one month" / "in N months"**)
→ churn line + Steps below — short horizons are N steps of that line, not a net-movement trend
- **Revenue to end of month/year** → Rest-of-period run-rates (do not run the MRR Steps)
- **Low/base/high or "N months out"** → Scenario forecasts (use the churn line when projecting
MRR/subs)
- **Underspecified** ("forecast", "predict the future", no metric/horizon) → default to a
**12-month MRR** projection and run the MRR Steps; say that default out loud. On annual-heavy
books the Status snapshot will not fill every forecast month — use the expiration months it
has, leave empty annual months at zero due (do not invent fill-in), keep rolling short-duration
due and inflows, and say that the annual schedule only covers the live book's next cycle.
## The churn line
For month-by-month MRR or ARR — including solving for the spend or new subscriptions needed to hit
a target, including when the user already gave you churned MRR, spend, or efficiency numbers, and
including short asks like "what will MRR be in one month" or "in two months" — churned MRR in a
month is:
churned MRR[m] = MRR up for renewal in m × (1 − renewal rate)
### Short horizons (1–2 months, or any small N)
"In one month" / "in two months" / "by next month" is still the churn line — **N forward steps**,
not a rest-of-period run-rate and not an average of recent net MRR movement.
1. Read Status `expiration_month` **Total MRR** for every calendar month that overlaps the window
(e.g. remaining current month + next month for ~30 days out; two full months for "in two
months").
2. Each step: `churn[m] = due[m] × (1 − rate)` per duration; roll P1M/P3M due past the snapshot;
leave empty annual months at zero due.
3. Add inflows from recent new/resub/expansion only.
4. Step the stock: `MRR[m+1] = MRR[m] − churn[m] + inflow[m]` — once for N=1, twice for N=2, …
5. One script. Net movement / frozen churned MRR may appear as a diagnostic gap only — never as a
second method that sets the headline number, and never `annual_base / 12`.
Correct — one month ahead (same idea for N=2 with two months in the loop):
# due from Status expiration_month (measure=mrr), not base/12
due_annual_next = 28316.52 # e.g. Oct Total MRR for P1Y
due_monthly = monthly_base # standing P1M book
churn = due_annual_next * (1 - r_annual) + due_monthly * (1 - r_monthly) + ...
mrr_next = mrr_now - churn + inflow_run_rate
Get the **renewal rate from Subscription Retention or Cohort Explorer** (chart literals only) —
not from `observed_churned ÷ base`, not from Set-to-Renew %. The rate applies to the **due slice**
for that duration (for monthlies due ≈ stock; for quarterlies and longer terms, never take
`churned ÷ full base` and then apply it to the due slice — that understates churn).
**Get "MRR up for renewal" from Subscription Status** for the current cycle of the live book:
get-chart-data(
project_id="<id>",
chart_name="subscription_status",
realtime=true,
resolution="<id from options schema>",
start_date="<YYYY-MM-DD>",
end_date="<YYYY-MM-DD>",
segment="expiration_month",
selectors="{\"measure\": \"mrr\"}", # JSON string, not an object; not active_subscriptions
limit_num_segments=24, # so later months are not folded into Other
# optional: filters="[{\"name\": \"product_duration\", \"values\": [\"P1Y\"]}]"
)
Use the same `get-chart-data` pattern for Retention / Cohort Explorer / MRR / MRR Movement — but
**do not** copy Status-only fields (`segment=expiration_month`, `limit_num_segments`,
`selectors.measure`) onto those charts; take segment/filters/selectors from each chart's options
schema (`get-chart-options-schema` with `"realtime": true`).
Each segment is a calendar month when currently active subscriptions' periods end. Use **Total
MRR** only as `due[m]`. Set to Renew / Cancel / Billing Issue are near-term intent labels — not
forecast renewal rates. Status is a snapshot of the live book only — still model new / resub /
expansion from MRR Movement (or the user's spend plan) as inflows. If you cannot pull due at all,
ask for the renewal schedule rather than using `annual_base / 12`.
### Due and rate by duration
| Duration | Due (Status available) | Due (Status unavailable) | Rate |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| **P1Y** | Total MRR by `expiration_month` (first + repeat of the live book). Empty far months stay empty — do not invent fill-in. | Prior-year P1Y new + resub in M (misses repeat anniversaries — state that) | Retention Y1 / Cohort (`subscription_type = new` when using Retention) |
| **P1M** | Near-term expiration buckets, then roll ≈ standing monthly base each later month | Standing monthly base | Retention / Cohort (or movement `churned / due` where due ≈ base) |
| **P3M / other fixed** | Near-term buckets, then roll ≈ base / term-months | base / term-months | Retention / Cohort (or movement `churned / due`, never `churned / full base`) |
Wrong — annual due as a flat fraction of the stock:
annual_due = annual_base / 12 # or due_frac = {"annual": 1/12}
Wrong — back-solving a residual so modeled churn matches observed:
repeat_annual = observed_annual_churn - annual_due[m] * (1 - rate)
churn_annual = annual_due[m] * (1 - rate) + repeat_annual # then projected
Wrong — fixed-term rate on the full stock:
rate = 1 - churned_P3M / p3m_base # then churn = (p3m_base/3) * (1 - rate) → understates
Wrong — second model after a gap (blended / net movement / "calibrated" churn):
# after due*(1-rate) disagrees with last month's churned MRR:
project(net_fn) or mrr *= (1 - blended_churn) + inflow # never
Correct:
# due: subscription_status, measure=mrr, segment=expiration_month, filter product_duration=P1Y
annual_due = {"2026-11": 17.32, "2027-02": 6.66, ...} # Total MRR per expiration month
# rate: Subscription Retention Y1 or Cohort Explorer — not Set-to-Renew %
churn_annual[m] = annual_due[m] * (1 - renewal_rate_annual)
# monthly: expiration-month for the next cycle, then due ≈ standing monthly_base thereafter
churn_monthly[m] = monthly_due[m] * (1 - renewal_rate_monthly)
# quarterly: due ≈ p3m_base/3; rate from Retention (or churned_P3M / due, not / p3m_base)
## Steps (month-by-month MRR/ARR only)
Do not use these steps for rest-of-period revenue run-rates — use that section instead.
1. Pull MRR segmented by `product_duration` for the current stocks.
2. Pull **Subscription Status** as in the `get-chart-data` example above (selectors
`{"measure": "mrr"}`, `segment=expiration_month`, high `limit_num_segments`), filtered by
`product_duration` for each duration you model. Read **Total MRR** per expiration month into
the script as `due` only. Roll P1M/P3M due past the snapshot per the table; leave empty annual
months empty.
3. Pull rates from **Subscription Retention or Cohort Explorer** — not Set-to-Renew % from status.
Every rate literal in code must appear in a prior tool output.
4. Pull MRR Movement for **new / resub / expansion run-rates only** (inflows). Do not feed
churned MRR, net movement, or a blended churn rate into the projection. Churn in the script
must be `due[m] × (1 − rate)` — never `project(net_fn)` / a fit on net MRR movement /
`mrr * (1 - blended_churn)`.
5. Compute in **exactly one** script shaped like the Correct block. If month-1 modeled churn ≠
observed, print the gap as a diagnostic only — do not run a second, calibrated, blended, or
net-movement projection, and do not add a residual. The renewal-due result is the central
answer.
6. Present that central case, the range and what drives it, and the gap diagnostic if any. Call a
number a floor or ceiling only if every stated assumption biases it that way.
## Rest-of-period run-rates
For "revenue until the end of the month/year" and similar — not the MRR Steps above:
- Today is incomplete. Exclude it from the daily average, or say that you excluded it.
- Give a range or state the observed day-to-day volatility. A bare point estimate is not a
forecast.
- Triangulate two methods — elapsed-day pace, and the prior period's shape applied to the current
one — and say which you weighted and why.
- Prefer splitting out MRR already locked via Subscription Status expiration months in the
remainder of the period, rather than assuming renewals are spread evenly.
- Put the headline numbers in the chat answer; do not defer them to a chart or artifact.
## Scenario forecasts
For low / base / high cases or "N months out":
- Each case names the driver that differs (acquisition rate, renewal rate, a pricing change
working through the base), not a multiplier on the same number.
- Commit to a central case; a range alone does not answer the question.
- Every renewal or retention rate in the simulation comes from a chart pull for this account.
Published benchmarks are context, not inputs.
- "N months out" ends N months from today. If the model is anchored on the last complete month,
say so and name the end month.
- When the metric is MRR or subscribers, use the churn line above for the retention side.
## Prediction Explorer
Prediction Explorer models cohort LTV, not churn. Use it for payback or LTV cross-checks when the
question asks for them; it does not replace the churn line above.
revenuecat-identify-user4.51 KB
---
name: revenuecat-identify-user
description: Tie RevenueCat identity to your app's auth system. Use when the user asks to log in to RevenueCat, sync a user with RevenueCat, switch RevenueCat user on login, log out of RevenueCat, move a user from anonymous to identified, set appUserID, or handle account switching on iOS, Android, Kotlin Multiplatform, Flutter, or React Native.
---
# revenuecat-identify-user: connect RevenueCat to your auth system
Use this skill when the user wants to call `logIn` / `logOut` on the RevenueCat SDK so that their app users line up with RevenueCat subscribers. This skill does not cover initial SDK setup (see `integrate-revenuecat`), purchases (`revenuecat-purchase-flow`), or gating (`revenuecat-entitlements-gate`).
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency → read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root → read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*` → read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP) → read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root → read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
## 2. Shared concepts (all platforms)
- **Anonymous by default.** Before `logIn` is called, RevenueCat assigns a stable anonymous ID prefixed `$RCAnonymousID:`. Purchases made while anonymous are aliased onto the real `appUserID` the first time `logIn` is called with it, so there is no "lost purchase" risk from letting users buy before signing in.
- **Never use email, phone number, or a sequential database id as the appUserID.** Use a stable opaque value such as your backend's user UUID, or a hash of the user id. RevenueCat treats the ID as an opaque string and it is difficult to change later.
- **Call `logIn` after your auth system confirms the session.** Do not call `logIn` speculatively. The typical trigger is your auth state listener firing with a signed in user. `logIn` returns both the user's current `CustomerInfo` and a `created: Boolean` that tells you whether this is a brand new RevenueCat customer.
- **`logOut` only works on identified users.** Calling `logOut` while the SDK is on an anonymous ID throws an error in every SDK (`PurchasesErrorCode.LogOutWithAnonymousUserError` or the iOS equivalent). Gate it behind your own "is signed in" flag.
- **Restore is not login.** `restorePurchases()` asks the store for the current receipt and attaches it to the current RevenueCat user. It does not switch identities. If the user signs in on a new device, call `logIn(appUserID)` first, then `restorePurchases()` only if they also expect to pull a receipt from the current store account.
- **Account switching is `logOut` then `logIn`.** If your app lets a user sign out and sign back in as someone else, call `logOut()` first, wait for it, then `logIn(newId)`. Do not try to swap directly with a second `logIn`, since that will alias the two IDs together.
- **Configure first.** `Purchases.configure(…)` must have run before `logIn` / `logOut`. If it has not, the SDK throws.
## 3. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
Each platform file shows the `logIn` and `logOut` calls wired into a typical auth state observer.
## 4. Verify
Do not claim identity sync works until:
1. In the RevenueCat dashboard, the Customer page for your test user shows the **same** appUserID your backend uses, not the `$RCAnonymousID:` placeholder.
2. Signing out clears the ID back to a fresh anonymous user; signing in as a different account switches to that account's purchases (or shows none if it is a new account).
3. A purchase made while anonymous, followed by `logIn`, remains attached to the signed in user (aliased, not lost).
4. Calling `logOut` while already anonymous is handled, not treated as a crash or a silent success.
Referenced files: 5
revenuecat-migrate6.64 KB
---
name: revenuecat-migrate
description: Migrate to RevenueCat from raw StoreKit or Google Play Billing, or upgrade the RevenueCat SDK across a major version. Use when the user says migrate to RevenueCat, switch from StoreKit to RC, upgrade RevenueCat SDK, from v4 to v5, observer mode, RevenueCat major version upgrade, or already have in app purchases and want to add RevenueCat on iOS, Android, Kotlin Multiplatform, Flutter, or React Native.
---
# revenuecat-migrate: migrate to RevenueCat or upgrade the SDK
Use this skill when the user wants to either adopt RevenueCat in an app that already ships in app purchases, or upgrade the RevenueCat SDK across a major version.
These two paths share some concepts but have different risks. Identify which one applies before touching code.
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency → read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root → read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*` → read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP) → read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root → read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
## 2. Identify the migration path
Ask the user (or infer from the codebase):
- **Path A: adoption.** The app already has working in app purchases implemented directly against StoreKit or Google Play Billing. RevenueCat is being added on top.
- **Path B: version upgrade.** The app already uses RevenueCat, and the user wants to bump from one major version to the next (e.g. v4 to v5, v7 to v8).
Both paths can happen at once (e.g. adopt RC today on the latest major version). Run Path A first, then Path B if needed.
## 3. Shared concepts
### Observer mode (Path A)
Observer mode is the key lever for adopting RevenueCat without rewriting purchase code. The SDK **observes** transactions that your existing StoreKit / Billing code processes, sends them to the RevenueCat backend for validation, and updates subscriber state, but does **not** initiate or finish the transactions. Your existing purchase UI, receipt validation, and transaction finishing stay in place.
Set this at configure time:
- iOS: set `purchasesAreCompletedBy: .myApp` together with `storeKitVersion: .storeKit1` (or `.storeKit2`) on `Configuration.Builder`. They are separate parameters, not a single associated value.
- Android: `purchasesAreCompletedBy(PurchasesAreCompletedBy.MY_APP)` on `PurchasesConfiguration.Builder`.
- Flutter: pass `const PurchasesAreCompletedByMyApp(storeKitVersion: StoreKitVersion.storeKit2)` to `PurchasesConfiguration`.
- React Native: pass `purchasesAreCompletedBy: { type: PURCHASES_ARE_COMPLETED_BY_TYPE.MY_APP, storeKitVersion: STOREKIT_VERSION.STOREKIT_2 }` in the configure call.
The default is RevenueCat completed (`REVENUECAT` / `.revenueCat`), where the SDK owns the full flow.
Once stable in observer mode, you can optionally cut over to full RevenueCat mode later by removing your own purchase plumbing and dropping the `purchasesAreCompletedBy` override.
### Do not double process transactions
When `purchasesAreCompletedBy` is set to `myApp`, RevenueCat does **not** finish transactions on iOS or acknowledge on Android. Your existing code must continue to do that. If you remove the `myApp` flag while leaving your old transaction finishing code in place, transactions get acknowledged twice and subscriber state can appear inconsistent.
Exactly one side must own finishing / acknowledging. Pick a side and remove the other.
### User continuity (Path A)
If the app already has its own authentication system, call `Purchases.logIn(existingAppUserID)` once RevenueCat is configured. This attaches the prior purchase history to the right RevenueCat user on ingestion. Without this step, existing purchases get recorded against an anonymous RC user and cannot be matched to the app's actual user records later.
Only skip this if the app has no notion of authenticated users.
### Version bumps change required fields (Path B)
Major version upgrades change configuration shape, drop deprecated APIs, and shift default behavior in ways that move with each release. This skill does not duplicate the per-version diff. Read the canonical sources from the SDK repo:
- **CHANGELOG**: in the relevant SDK repo on GitHub. Walk entries from your installed version up to the target.
- **Migration guides**: search the SDK repo for files matching `*MIGRATION*.md` or a `migrations/` directory. Major bumps usually ship a dedicated guide there. The release notes for the major version on the repo's GitHub releases page typically link to it.
- **Release notes**: each major version's release notes on the repo's GitHub releases page summarize the breaking changes.
Treat the SDK repo's docs as authoritative. Any version-specific diff written here would drift out of date. The platform file under `platforms/` for your target lists the exact repo to consult.
### Plan then migrate
Work in this order on every platform:
1. Bump the SDK to the new major version in a branch.
2. Fix compile errors using the CHANGELOG deprecations and removals as a guide.
3. Fix runtime behavior by reading the SDK logs on first launch.
4. Run the existing test suite and manual sandbox scenarios before merging.
## 4. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
Each platform file covers both migration paths for that platform.
## 5. Verify
Do not declare migration done until:
1. The app builds on the new SDK version with no warnings from deprecated APIs you care about.
2. A sandbox purchase succeeds and the transaction shows up on the RevenueCat dashboard Sandbox view with the expected appUserID.
3. An existing subscriber from before the migration opens the app, and their entitlement state is correct. For Path A this proves the observer mode ingest worked. For Path B this proves the version bump did not drop state.
4. You have removed the debug log level override before shipping.
Referenced files: 5
revenuecat-paywall5.07 KB
---
name: revenuecat-paywall
description: Display a RevenueCat paywall inside an app using the RevenueCatUI SDK. Use when the user asks to add a paywall, show a RevenueCat paywall, present PaywallView, integrate RevenueCatUI, gate a premium screen with a paywall, launch PaywallActivity, call presentPaywall or presentPaywallIfNeeded, or show the dashboard configured paywall UI on iOS, Android, Kotlin Multiplatform, Flutter, or React Native. To create, edit, evaluate, or audit the dashboard paywall itself, use revenuecat-paywall-design.
---
# revenuecat-paywall: display a RevenueCat paywall
Use this skill when the user wants to show a paywall that is built and configured in the RevenueCat dashboard, using the native RevenueCatUI components. This skill does not cover building a custom paywall from scratch. For that, use `revenuecat-purchase-flow` (when available) and `Purchases.getOfferings(…)` directly. To create, edit, evaluate, or audit the dashboard paywall, use `revenuecat-paywall-design`.
Prerequisite: `integrate-revenuecat` has already run. `Purchases.configure(…)` must succeed before a paywall can load.
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency. `react-native-purchases-ui` is the paywall package. Read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root. The paywall package is `purchases_ui_flutter`. Read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*`. The paywall module is `purchases-kmp-ui`. Read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP). The paywall dependency is `com.revenuecat.purchases:purchases-ui`. Read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root. The paywall product is `RevenueCatUI`. Read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
## 2. Shared concepts (all platforms)
- **Paywalls require an Offering with a paywall attached in the RevenueCat dashboard.** The SDK pulls offerings via `getOfferings()`. If no offering has a paywall configured, RevenueCatUI falls back to a default paywall layout, which is not what you want in production.
- **Offering vs. entitlement.** Users purchase a product through a package in an offering. Access is granted via an entitlement (typically `"premium"` or `"pro"`). Gate premium features on the entitlement, not on the offering.
- **Three presentation patterns**:
- (a) First launch modal for users without the entitlement, typically driven by a "present if needed" helper that checks the entitlement and only shows the paywall when missing.
- (b) Gated premium screen. The user taps a premium feature and the paywall opens before the screen loads.
- (c) Conditional present on a CTA tap, such as an "Upgrade" button in settings.
- **RevenueCatUI owns the purchase flow.** Do not call `Purchases.purchase(…)` manually alongside a RevenueCatUI paywall. The paywall calls it internally. Listen for the dismiss or purchase completed callback to react in app code.
- **Close button is opt in on most platforms.** Pass `displayCloseButton = true` (iOS / Flutter / RN) or `setShouldDisplayDismissButton(true)` (Android / KMP) when the paywall is presented modally and the user needs a way out. Skip it when presenting behind a sheet with its own grabber, or when wrapping the paywall in a navigation controller.
- **If the app needs a fully custom UI**, do not use this skill. Call `Purchases.getOfferings()` and render your own components. RevenueCatUI is only for dashboard templated paywalls.
## 3. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
Each platform file is self contained: install command, exact snippet to present the paywall, and the callback shape you listen to.
## 4. Verify
Do not claim the integration is complete until:
1. The project **builds** on the target platform.
2. The app launches, the code path that presents the paywall runs, and the paywall UI renders with the template configured in the dashboard (not the default fallback layout).
3. Tapping a package and completing a sandbox purchase dismisses the paywall and fires the purchase completed callback (or, for imperative APIs, resolves with a `PURCHASED` result).
4. Closing the paywall without purchasing fires the dismiss / cancelled callback.
If the paywall shows the default fallback layout instead of your template, the offering does not have a paywall attached in the dashboard. Fix this in the dashboard, then retry.
Referenced files: 5
revenuecat-paywall-design13.7 KB
---
name: revenuecat-paywall-design
description:
Must invoke for any request to create, edit, evaluate, understand, or improve a RevenueCat
Paywall in the dashboard — including questions about a specific paywall or general paywall
design recommendations. Not for presenting a paywall in app code (use revenuecat-paywall).
---
# Designing and editing RevenueCat Paywalls
This skill covers the **dashboard paywall**: creating, duplicating, editing, auditing, and
publishing. To **display** a configured paywall in the app with RevenueCatUI, use
`revenuecat-paywall` instead.
Paywall advice requires a paywall. An offering whose `paywall_id` is null has no RevenueCat
paywall: its purchase UI is client-side and invisible to you. Say so, and keep your advice to
what RevenueCat actually controls.
Via the `rc` CLI (see the `revenuecat-cli` skill): `rc paywalls generate --prompt "..."`,
`rc paywalls edit --prompt "..."`, `rc paywalls rewind --session <id>`, `rc paywalls publish`,
`rc paywalls show`. Prefer MCP below when you need screenshots or a structured `get-paywall`
read.
## Evaluating a paywall
When evaluating a RevenueCat Paywall, use `render-paywall-screenshot` to look at a static render
of above-the-fold content. The render is for you, not something to display to the user.
For a specific identified paywall, match the tool to the question:
- **Exact configuration** (default package, component bindings, offer-conditional content):
`get-paywall` with `expand: ["components"]`. Without that expand, component configuration is
omitted.
- **Visual inspection**: `render-paywall-screenshot`. A screenshot cannot verify
configuration-dependent details.
- If a tool answer conflicts with what the user reports seeing, verify with `get-paywall` before
disputing their observation; if `get-paywall` is unavailable, say you could not verify rather
than disputing them.
Both `get-paywall` and `render-paywall-screenshot` use the draft when available, otherwise the
published version, and cover one paywall — not workflows or multipage paywalls. For general
paywall advice that is not about a specific paywall, answer directly without calling these tools.
If `get-paywall` fails, you may still report observations explicitly visible in a screenshot.
State that configuration-dependent details were not verified; do not silently replace
structure-aware analysis with screenshot reasoning.
## Editing
When the user explicitly asks you to edit an existing paywall, use `edit-paywall-ai`. It starts
an async task: poll `get-paywall-ai-task` with the returned task ID about every 10–15 seconds
until it succeeds or fails. Do not call `edit-paywall-ai` again for the same request unless the
task fails. Image-generation edits can take several minutes.
If the user wants to edit manually, link to the paywall builder:
```
https://app.revenuecat.com/projects/{project id without leading proj prefix}/paywalls/{paywall ID starting with pw}/builder
```
The builder also has a conversational AI editor in the left toolbar; you cannot link to it
directly.
If the user attaches images to the current message and the tool schema accepts them, pass them
through and tell the editor how they should be used. Do not reproduce visual details
unnecessarily. Images from earlier messages are not still attached — ask the user to attach
again.
MCP has no restore-version tool. If the user asks to undo an edit you made via MCP, tell them
they can restore an older version in the paywall editor. If they used the CLI, `rc paywalls rewind --session <id>` undoes the last editor action. Restoring does not publish.
## Duplicating
When the user asks for a paywall that copies, duplicates, or mirrors an existing one, use
`duplicate-paywall` rather than `create-paywall-ai`. `create-paywall-ai` designs from a blank
canvas and a text prompt, so it reproduces only what the prompt describes: images, exact
component structure, and anything unmentioned are regenerated rather than copied.
`duplicate-paywall` copies the source paywall — its current draft by default, or its published
version if you pass `source_version: published` (fails if the paywall has never been published)
— into a new unpublished paywall. Choose deliberately:
- When the source has a published version (`get-paywall`'s `published_at` is non-null), pass
`source_version: published` — that is the paywall the user means when they reference one by
name or link.
- Take the `draft` default only when the user is explicitly asking to copy in-progress
unpublished edits, or the source has never been published.
Say which version you copied. Pass `offering` (`lookup_key` and `display_name`) to also
duplicate the source paywall's offering — packages only — and attach the copy to it; omit
`offering` to leave the copy unattached. When the user's framing is offering-first — a second
offering that should carry the same paywall — use `duplicate-offering` instead, which copies the
packages and the attached paywall together. Then apply differences with `edit-paywall-ai`, and
describe the result as a copy of the original rather than as a new design.
Duplication does not cover every paywall — multipage paywalls are not supported. If duplication
is unavailable or fails, you may fall back to `create-paywall-ai`, but tell the user the result
is a reconstruction from a description, not a copy, and that details such as images will differ.
## Creating
When the user asks you to create a new paywall, use `create-paywall-ai` to create a **draft
only**. It starts an async task: poll `get-paywall-ai-task` about every 30 seconds until it
succeeds or fails. Image-heavy creation can take 5–10 minutes; do not call `create-paywall-ai`
again for the same request unless the task fails.
Prefer creating the paywall for an existing offering when the user names one or when you can
confidently identify a suitable offering. If they do not name an offering, use `list-offerings`,
`get-offering`, `get-offering-prices`, `list-products`, `list-apps`, `get-app`,
`get-project-ui-config`, and `list-paywalls` to understand the app, products, pricing, brand
conventions, and existing paywalls first. Inspect the app/codebase and pass concise
`codebase_context` (premium features, brand colors, visual direction, tone, products, audience)
and `app_context` when you can infer it. Recommend the best offering when there is a clear fit;
if the user declines or no clear offering is known, create an unattached draft.
An offering can have at most one attached RevenueCat Paywall. If the offering has a
`paywall_id`, do not pass that offering as `offering_id` to `create-paywall-ai`. Explain that it
already has a paywall, and pick the option that matches:
- The new paywall should look like the existing one: duplicate it. This is the right choice
whenever the user said "same as", "like", "copy", or "mirror".
- The existing paywall should change: `edit-paywall-ai`.
- The new paywall is a genuinely different design: choose a different or unattached offering, or
create an unattached draft using the offering's products and pricing as context.
Do not present an unattached `create-paywall-ai` draft as the way to satisfy a duplication
request.
Fold gathered context into the prompt: app identity, target audience, core value proposition,
product durations, trials, price points, existing paywall patterns, brand colors or fonts, and
any user-stated design direction. Keep the prompt concrete enough for a one-shot draft and do
not ask the tool to publish.
A duplicated or newly created paywall is an unpublished draft. Do not publish unless the user
explicitly asks, except when preparing a treatment offering for an experiment (see
`revenuecat-experiments`).
# Overall ideas and guidance
## Collecting ideas
Search the RevenueCat blog and docs for paywall design and best practices. Markdown docs are at
the page URL with `.md` appended.
## Disallowed practices
Apple's App Review guidelines are strict. Patterns often touted as best practices can lead to a
rejected update or a store ban:
- **Trial toggle.** A toggle to enable/disable the trial. Apple rejects this as misleading.
- **Delayed close button.** A dismissible paywall that cannot be closed for a few seconds.
Apple considers this misleading.
- **Second offer on dismiss / purchase cancel.** Least clear of the four; showing the exact
same product after cancelling has been rejected as tricking users into unwanted purchases.
- **Non-IAP digital goods in-app.** Stripe or other non-IAP SDKs in the app or an in-app
webview for digital goods. Linking out to web purchases in an external browser is a different
(US) path; the purchase must not happen inside the app.
## Paywall placement
Try different placements: beginning or end of onboarding, as a feature gate, etc. Onboarding
paywalls tend to be very successful: they capture the highest number of potential customers
(nobody has churned yet) and motivation is high just after download.
## Always experiment
Results of different paywall designs are highly variable. Continuously test. At thousands of
new users per day, A/B testing with RevenueCat Experiments is the best validation. At hundreds
per day, tests can still be directional. Below that, ship and observe conversion over time. Use
the `revenuecat-experiments` skill to set up a test.
# Auditing existing paywall designs
Analyze the paywall and identify conversion issues. Prioritize conversion over aesthetic
consistency when they conflict. Audit in this order of impact.
### Visual issues (highest impact — fix these first)
**CTA Button** — single highest-impact element, address this FIRST.
- **CTA color is the #1 priority.** Look at the CTA button background color, then scan every
other colored element (icons, badges, borders, card accents). If the CTA shares the same hue
as ANY of them, it must change to a contrasting color that nothing else on the page uses. A
CTA that blends in with the page accent kills conversion.
- If the CTA copy is generic ("Continue", "Subscribe"), it needs to be specific and
action-oriented (e.g. "Start Yearly Plan", "Get Unlimited Access").
- If a free trial exists and the CTA does not mention it, the CTA should include trial language
(e.g. "Start Free Trial", "Try for Free"). Trial info buried in small print is a conversion
miss.
**Plan Card Differentiation**
- The recommended plan MUST be impossible to miss. It needs at least TWO of: a different
background color, a colored border, a "Best Value" / "Most Popular" badge, or a larger/
elevated card. A single subtle border change is not enough.
- If there is no default plan selected, one needs to be set.
- Equal-weight cards cause decision paralysis — differentiate the recommended plan.
- If a badge overlaps text, the selection indicator, or other content, reposition it.
**Price Anchoring** — required when multiple plans exist.
- If savings are not immediately visible (no crossed-out price, no "Save X%", no per-day
comparison), the recommended plan needs a savings signal.
- The signal must be concrete. Vague phrases like "Save with annual billing" do not count. Show
real values: a computed discount, a free-period callout, or a per-period price comparison.
Never hardcode discount percentages or savings amounts.
- Use ONE clear anchoring signal. Stacking crossed-out price + savings badge + absolute
discount + original price creates visual noise.
**Price Visibility.** Every package card MUST display its price.
**Content Reduction.** Count content rows between the headline and the packages/CTA. If there
are more than 4–5, remove the weakest. Every row above the fold competes with the CTA.
**Layout.** Packages and the CTA MUST be above the fold. Keeping the purchase action (and
ideally the packages) always visible regardless of scroll is the strongest pattern. Never
remove the purchase button or the privacy footer.
- The package group and the purchase button must be adjacent. Content between them should move.
- There must be visible spacing between packages and the purchase button (at least 12px).
**Trust Signals** — required. Every paywall MUST have a short reassurance line below the
purchase button (between the button and the privacy footer): "Cancel anytime", "No commitment",
trial terms. Order: packages → purchase button → reassurance text → privacy footer. If a free
trial exists, trial terms should be visible near the CTA (e.g. "7-day free trial, then $X/month").
### Trial presentation (when a free trial exists)
- Mention the trial in the CTA. "Start Free Trial" converts better than "Subscribe" with trial
details in small print.
- The trial should be in the headline, the CTA, or both — not just legal text.
- For a multi-day trial (7+ days) using a trial-timeline archetype, check that steps are clear:
start date, reminder date, charge date.
- If trial terms (length, price after trial) are not visible anywhere, add them near the CTA.
### Anti-pattern checks
- **Generic CTA copy**: "Subscribe", "Continue", or "Choose" tells users what to DO, not what
they GET. Rewrite to action + benefit.
- **Identical feature lists on multiple plan cards**: remove the duplicates; comparison should
be pricing only.
- **Negative-only value proposition**: "No ads, No interruptions, No limits" sells removal.
Rewrite at least some items to what users GAIN.
- **Legal text as primary content**: terms and renewal policies drowning the sales pitch belong
behind links. This does NOT apply to the privacy footer.
- **Missing CTA button**.
- **Missing privacy footer** with Terms and Privacy Policy links — App Store compliance.
### Copy issues (fix these last)
- Generic or feature-led copy → outcome-led. "Access all features" → "Master any language".
- Paragraphs longer than two lines should be shorter or become bullets.
- Headlines that describe the product → address the user. "AI-powered photo editor" → "Make
every photo stunning".
revenuecat-purchase-flow4.17 KB
---
name: revenuecat-purchase-flow
description: Implement the RevenueCat purchase and restore flow. Use when the user asks to buy a package, purchase a subscription, fetch offerings, build paywall purchase logic, handle purchase errors, detect user cancelled, or restore previous purchases on iOS, Android, Kotlin Multiplatform, Flutter, or React Native.
---
# revenuecat-purchase-flow: buy a package and restore purchases
Use this skill when the user wants to complete the purchase side of RevenueCat: fetch offerings, call `purchase`, deal with cancellation and errors, and expose a "Restore" action. It does not cover rendering a paywall UI (that lives in `revenuecat-paywall`) or gating features (that lives in `revenuecat-entitlements-gate`).
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency → read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root → read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*` → read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP) → read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root → read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
## 2. Shared concepts (all platforms)
- **Flow.** Call `getOfferings()`, pick a `Package` from the current offering, call `purchase(package)`. When it completes successfully, the returned `customerInfo` already reflects the purchase. Read `customerInfo.entitlements.active["<id>"]` to confirm access.
- **User cancellation is not an application error.** Each SDK surfaces it differently: iOS throws a `purchaseCancelledError` code, Android throws a `PurchasesException` with `PurchasesErrorCode.PurchaseCancelledError`, Flutter surfaces a `PlatformException` with that same code, React Native sets `e.userCancelled === true`. Return silently in this case. Do not show an alert.
- **Errors worth messaging.** Payment declined, network errors, store unavailable, receipt already in use. Everything else should be logged and let the user try again. Never silently succeed when the purchase actually failed.
- **Do not unlock content inside the purchase callback.** Refresh customer info and let your entitlements listener (see `revenuecat-entitlements-gate`) flip the gated UI. This keeps one source of truth for access and avoids drift between the purchase path and the restore path.
- **`restorePurchases()` is a user action, not an automatic step.** It asks the store for the current receipt and syncs it to RevenueCat. Expose it from a visible "Restore purchases" button on the paywall and/or settings screen. Legal requirements on iOS mandate such a button.
- **One purchase at a time.** Disable the paywall buy buttons while a purchase is in flight to prevent double charges.
## 3. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
Each platform file contains a complete purchase function and a restore function.
## 4. Verify
Do not claim the flow works until:
1. A sandbox purchase of the current offering's package succeeds end to end, and the user's entitlement flips to active.
2. Cancelling the store sheet does not show an error alert and does not leave the UI in a loading state.
3. A second purchase attempt for the same active subscription is handled cleanly (StoreKit / Play Billing will surface a `productAlreadyPurchased` / `receiptAlreadyInUse` path; the flow should not crash).
4. The restore button, on a fresh install signed in to the same store account, restores the entitlement and updates the UI.
Referenced files: 5
revenuecat-sdk-compatibility2.89 KB
--- name: revenuecat-sdk-compatibility description: Check whether an SDK version supports a RevenueCat feature, whether an SDK upgrade is required, why an SDK-gated feature is not working, or how much of a project's recent subscriber base is on incompatible SDK versions. Use for questions about RevenueCat SDK compatibility, feature gates, minimum SDK versions, SDK adoption, upgrade impact, and rollout risk. --- # RevenueCat SDK compatibility Use the RevenueCat MCP server for all tool calls here. ## Check feature compatibility Call `list-sdk-feature-gates` before making any SDK compatibility claim. Find the gate that matches the user's feature and use its minimum version for each relevant RevenueCat SDK. - If the relevant feature gate cannot be identified, say so and explain which gates looked close instead of guessing. Consult the docs linked in the tool output to disambiguate. - When more than one gate could apply, prefer the most specific gate. For example: - Existing-customer experiment enrollment: `experiment-existing-customer-enrollment` - Experiment exposure status or reporting: `experiment-exposure-tracking` - New-customer experiment enrollment: `experiment-new-customer-enrollment` - If multiple gates jointly matter, evaluate and name each one. ## Analyze SDK adoption Call `list-sdk-versions` to assess upgrade impact or rollout risk from SDK versions observed in the project. Each row describes subscribers seen in the last 30 days and includes: - `sdk_type`: RevenueCat SDK family. - `sdk_version`: underlying native SDK version for native rows, or the native SDK bundled under a wrapper in `platform_flavor` rows. - `sortable_sdk_version`: normalized value for ordering `sdk_version`. - `platform`: native platform, such as `ios` or `android`, when grouped by platform or platform flavor. - `platform_flavor` and `platform_flavor_version`: wrapper SDK family and version, such as `react-native` `10.0.1`, when grouped by platform flavor. - `subscribers_last_30days` and `pct_of_subscribers`: recent project usage counts and shares. Choose the grouping that matches the question: - Use `sdk_type` for a high-level inventory by RevenueCat SDK family. - Use `platform` to compare native iOS and Android adoption. - Use `platform_flavor` for React Native, Flutter, Capacitor, Unity, Kotlin Multiplatform, and other wrappers where both the wrapper version and bundled native SDK versions matter. To compute upgrade impact, filter to the relevant SDK family, compare observed versions with the feature gate's `min_version`, and sum `subscribers_last_30days` and `pct_of_subscribers` for rows below the requirement. State that these figures represent subscribers observed in the last 30 days, not the all-time install base. Use `jq` for quick filtering or a code execution tool for version comparisons, grouping, and subscriber-share calculations when the output is too large to analyze reliably by inspection.
revenuecat-status2.43 KB
---
name: revenuecat-status
description: Get a quick overview of your RevenueCat project configuration including apps, products, entitlements, offerings, and webhooks.
---
# RevenueCat Status
Get a quick overview of your RevenueCat project configuration.
## Description
This command provides a summary of your RevenueCat project including:
- Number of apps and their platforms
- Total products configured
- Entitlements defined
- Offerings and their packages
- Webhook integrations
## Usage
```
/revenuecat-status [project_name]
```
**Arguments:**
- `project_name` (optional): Name of the project to show status for. If not provided, shows status for all accessible projects.
Can be referenced as `$ARGUMENTS` in the skill.
## Instructions
Use the RevenueCat MCP server or the `rc` CLI for all tool calls (see the `revenuecat-cli` skill). Each `list-*` tool below has an `rc <noun> list` equivalent (e.g. `list-offerings` → `rc offerings list`; note `list-webhook-integrations` → `rc webhooks list`).
When the user invokes this skill, perform the following steps:
1. **Parse Arguments** (from $ARGUMENTS)
- Extract `project_name` (optional)
- Project name matching is case-insensitive and supports partial matches
2. **Get Projects**
- Use `list-projects` tool to retrieve all accessible projects
- If `project_name` is specified in arguments, filter projects by name (case-insensitive partial match)
- If no matching project found, inform the user and list available projects
- If no `project_name` provided, show status for all projects
3. **Gather Statistics for Each Project**
For each project (filtered or all), use the following tools:
- `list-apps`
- `list-products`
- `list-entitlements`
- `list-offerings`
- `list-webhook-integrations`
4. **Present Summary**
Format the results as a clear status report:
```
📊 RevenueCat Project Status
============================
Project: {project_name} ({project_id})
📱 Apps: {count}
- {app_name} ({platform})
...
📦 Products: {count}
- {product_identifier} ({type})
...
🔑 Entitlements: {count}
- {entitlement_name}
...
🎁 Offerings: {count}
- {offering_name} (current: yes/no)
...
🔗 Webhooks: {count}
- {webhook_name} → {url}
...
```
5. **Highlight Issues** (if any)
- Products not attached to any entitlement
- Offerings without packages
- Apps without products
revenuecat-store-state6.04 KB
---
name: revenuecat-store-state
description: Use when the user wants to inspect or change the state of products in App Store Connect, Google Play Console, RC Billing, or Test Store (prices, availability, localizations, review screenshots) via RevenueCat product-store-state plans
---
# Managing product store state
The store is the source of truth for prices, availability, localizations, and offers. Reads are immediate. Writes go through a **product store state plan**: create a draft, compute a reviewable diff, then apply. Nothing reaches the store until the plan is applied.
MCP and the `rc` CLI (see the `revenuecat-cli` skill) use the same plan/apply model. Prefer whichever surface is available. Refer to the MCP tool schemas (or `rc commands --schemas --json`) for exact parameters.
There is at most one active (non-terminal) plan per project. List plans before creating a new one; if create fails because a plan is already in progress, fetch it, describe what is pending, and decide with the user whether to continue it or discard it.
## Reads
Always use `get-product-store-state` (CLI: `rc products show <product-id> --store-state`). Never infer store prices, availability, or trials from `get-product`, `list-products`, or offering data — those are RevenueCat catalog, not store state. Surface the response's `warnings`.
## Prerequisites
Store-state writes require store credentials connected to the RevenueCat project (an App Store Connect API key, or a Google Play service account with the **Manage store presence** permission) and a RevenueCat login with write access to the project. If a write fails with a credentials or permission error, report what is missing and how to fix it (dashboard app settings for store credentials) instead of retrying.
## Confirm before applying
Apply writes to production stores and can change what live customers see and pay. **Never call apply until you have shown the planned diffs and the user has explicitly confirmed.**
After planning succeeds:
1. Summarize `summary` and each `plan_items[*].diff` in plain language (before → after per field).
2. Report every warning (`severity`, `field`, `message`). Never apply if any `plan_items[*].warnings` has `severity: blocker`. Fix the desired state, update the plan, and re-plan until there are no item-level blockers.
3. Optionally share the dashboard review URL:
`https://app.revenuecat.com/projects/{project_id}/product-catalog/product-editor/review-changes?plan_id={plan_id}`
4. Wait for an explicit go-ahead. Confirm each product's change individually or as one clearly itemized batch — never fold unrelated changes into a single confirmation.
5. Only then call apply, and only if `actions` includes `apply`.
Reads do not need confirmation. Submitting products to Apple for review is a separate confirmed write (`submit-products-to-store`).
## Recommended write flow (MCP)
1. **Inspect first.** `get-product-store-state` on existing products before deciding the desired changes.
2. **Create a draft plan.** `create-product-store-state-plan` with one `desired_states` entry per product.
- Target an existing RevenueCat product with `product_id`, or pass `create_revenuecat_product` (`app_id`, `store_identifier`, `type`, `display_name`, and `title` — when the user gave only one name, use it for both).
- Set `store` to `app_store`, `play_store`, `rc_billing`, or `test_store` and populate the matching `store_state`.
- Put prices, availability, localizations, and (if the user provided one) the review screenshot in the desired state. Do not call `create-product-prices`, `upload-product-store-state-screenshot`, or `equalize-subscription-prices`.
- App Store subscriptions: send the prices to keep in `territory_prices` and include `common.pricing.equalize_missing_subscription_prices` with that `base_territory` so missing Apple territories are filled at apply. Do not send that field for App Store one-time products.
- App Store review screenshots: omit screenshot so apply can upload a blank placeholder unless the user supplied an image.
- RC Billing / Test Store: pricing is `common.pricing.currency_prices` (create-only; updating an existing currency is rejected).
3. **Plan.** `plan-product-store-state-plan`, then poll `get-product-store-state-plan` until status is `planned`, `planned_and_finished`, or `plan_errored`.
4. **Review with the user.** Follow [Confirm before applying](#confirm-before-applying). If planning failed, report `error_message` / per-item errors and the review URL; do not apply.
5. **Apply.** `apply-product-store-state-plan`. Then poll `get-product-store-state-plan` until `applied` or `apply_errored`. Report per-product `apply_status` and any `apply_error_message`. If apply fails because the store changed after planning, tell the user and start a new plan from the fresh state.
6. **Submit for review when ready.** App Store only: after apply succeeded, the change is in App Store Connect but not yet submitted to Apple. Offer `submit-products-to-store` for the products just applied (confirmed write). Skip for Play Store, RC Billing, and Test Store.
To abandon a draft or planned plan, `discard-product-store-state-plan`. To change a draft, `update-product-store-state-plan` then plan again.
## CLI equivalents
See the `revenuecat-cli` skill for install, auth, and schema discovery. Mapping:
- Read: `rc products show <product-id> --store-state`
- Create + compute diff: `rc products store plan <app-id> --file <csv|json>`
- Re-display a saved plan: `rc products store show <plan-id>`
- Apply / discard: `rc products store apply <plan-id>` / `rc products store discard <plan-id>`
CLI apply waits for completion (no separate poll). Still show the plan diff and wait for confirmation before `apply`. Confirm exact flags with `rc commands --schemas --json`.
## Do not use these MCP tools for writes
These may still appear in the MCP catalog. They write one product with no previewable plan. Prefer the plan tools above.
- `set-product-store-state`
- `get-product-store-state-operation`
- `create-product-prices`
- `upload-product-store-state-screenshot`
- `equalize-subscription-prices`
revenuecat-testing-setup7.54 KB
---
name: revenuecat-testing-setup
description: Set up a testing environment for RevenueCat purchases without charging real money. Use when the user says test RevenueCat purchases, sandbox setup, StoreKit configuration file, license tester, how do I test purchases without real money, set up sandbox account, internal testing track, or test paywall on iOS, Android, Kotlin Multiplatform, Flutter, or React Native.
---
# revenuecat-testing-setup: set up a testing environment for RevenueCat purchases
Use this skill when the user wants to test purchases against RevenueCat without charging real money. Each store has its own testing channel, and each channel has different fidelity and iteration cost.
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency → read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root → read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*` → read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP) → read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root → read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
## 2. Shared concepts (all platforms)
### You cannot charge real money during development
Each store has a dedicated testing channel. Choosing the right channel depends on what you want to test.
### Testing channels by fidelity vs iteration cost
Higher fidelity exercises more of the real purchase pipeline. Lower fidelity iterates faster.
- **RevenueCat Test Store (lowest fidelity, fastest iteration, deterministic).** A synthetic store hosted by RevenueCat. The SDK is configured with a `test_…` API key from the dashboard. Purchases open a Test Store dialog where you pick the outcome by hand: Successful Purchase, Failed Purchase, or Cancel. Purchases trigger entitlements, update `CustomerInfo`, and appear on the dashboard, but no Apple or Google call happens. Best for paywall iteration, integration tests, and CI smoke runs.
- **iOS StoreKit Configuration File (low fidelity, Apple synthetic).** Xcode stubs the store locally. Purchases succeed instantly with no App Store Connect round trip. Useful when you need StoreKit specific behavior. RevenueCat transactions in this mode may or may not appear on the dashboard depending on SDK version and intended routing, so it is not a faithful dashboard test.
- **iOS Sandbox (real sandbox Apple ID) / Android Internal Testing (license tester).** Real store backends, real RevenueCat dashboard ingestion, real receipts. Slower to iterate: build, install, wait for Play propagation or App Store Connect to register the product.
- **TestFlight (iOS) / Closed or Open Testing (Android).** Behaves very close to production. Receipts are production style. Transactions land in the **production** RevenueCat dashboard, not the Sandbox view.
- **Production.** Real money. Do not use for testing.
Start with the lowest fidelity that answers the question, then move up. UI and paywall iteration belongs in the fast channel. "Does my entitlement actually flip?" belongs in sandbox or internal testing.
### Test Store: when to reach for it before sandbox
RevenueCat's Test Store is a synthetic store provider configured per project on the dashboard. It produces real RevenueCat backend records (`CustomerInfo` updates, entitlement transitions, dashboard transactions) without calling App Store or Google Play. The price of that speed is fidelity. Test Store does not simulate Ask to Buy approval flows, region specific pricing, server side receipt validation specifics, store level review or rejection paths, or full subscription renewal cadence (Test Store renews up to five times on a compressed clock).
Use Test Store when you want to iterate on UI quickly, write integration tests with deterministic outcomes, or run CI checks. Move up to App Store Sandbox or Google Play Internal Testing for anything that exercises store side behavior.
**Setup.** In the dashboard, go to **Apps and providers** → _Test configuration_ section → **Test Store** → Create / Enable. The Test Store API key appears under **Project Settings → API keys** with a `test_` prefix. Configure the SDK with the test key in debug builds and the production key (`appl_…` / `goog_…`) in release builds. The platform files show the per platform key swap pattern.
Test Store keys must never ship to production. Gate the key behind your build configuration so release binaries cannot route through Test Store.
### Sandbox transactions appear separately on the dashboard
The RevenueCat dashboard has a toggle between **Sandbox** and **Production** views. Sandbox purchases only appear under Sandbox. If a test transaction does not appear there, the SDK is not reporting it (check API key, `configure` call, and network), or the purchase was against a StoreKit config file that does not hit RevenueCat's backend.
### Accelerated renewal in sandbox
Subscription renewals run on accelerated test clocks in sandbox. This lets you exercise renewal logic in minutes instead of weeks.
- iOS sandbox renewal cadence (per Apple's documentation): daily → every 3 minutes, weekly → every 3 minutes, monthly → every 5 minutes, 2 months → every 10 minutes, 3 months → every 15 minutes, 6 months → every 30 minutes, yearly → every hour. Subscriptions auto-renew a maximum of 6 times then expire.
- Android sandbox renewal cadence is documented in Google Play Console → Subscriptions testing. License tester subscriptions renew on the same accelerated clock.
### Use a fresh test user for first purchase flows
Purchase history is attached to the test account (sandbox Apple ID or license tester Gmail) and persists. Testing "first purchase" logic, introductory offers, or free trials against an account that has already used them produces misleading results. Create a new sandbox tester for clean first purchase scenarios.
### Confirm against the dashboard, not the device
A purchase succeeding on the device is not the same as RevenueCat recording it. Always confirm:
1. The transaction appears on the RevenueCat dashboard (Sandbox view).
2. The expected appUserID owns the transaction.
3. The expected entitlement is now active on that user.
If the device shows success but the dashboard shows nothing, something is wrong in the configuration path, not the store.
## 3. Implementation
Read the platform file that matches detection:
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
## 4. Verify
Your testing environment is set up once:
1. A test purchase succeeds end to end from a test user.
2. The transaction appears on the RevenueCat dashboard Sandbox view, attached to the appUserID you logged in with.
3. The expected entitlement is active on `customerInfo` after the purchase completes.
4. Restoring purchases on a fresh install of the app restores the entitlement to the same test user.
If any of those four steps fails, the environment is not ready. The `revenuecat-troubleshoot` skill covers the usual root causes.
Referenced files: 5
revenuecat-troubleshoot14.5 KB
---
name: revenuecat-troubleshoot
description: Diagnose and resolve RevenueCat integration issues — inspects dashboard configuration through the RevenueCat MCP, walks the SDK debug logs, and covers code-side gotchas. Use when the user says offerings are empty, products not loading, entitlement not active after purchase, paywall won't load, transactions not appearing, customer info shows no entitlements, sandbox purchase not working, or RevenueCat is broken on iOS, Android, Kotlin Multiplatform, Flutter, or React Native.
---
# revenuecat-troubleshoot: diagnose RevenueCat integration problems
Use this skill when the user reports a RevenueCat behavior that does not match expectations: empty offerings, missing products, an entitlement that does not unlock after a successful purchase, a paywall that fails to render, or sandbox transactions that never reach the dashboard.
This skill combines two angles:
1. **Code-side diagnosis** — turn on debug logging, walk a universal checklist, drop into platform specifics.
2. **Dashboard inspection** — use the RevenueCat MCP server or the `rc` CLI (see the `revenuecat-cli` skill) to read the project, apps, products, entitlements, offerings, and webhooks, and offer fixes.
Work them in order. Most reports resolve before you reach the platform specifics.
## 1. Detect the platform
Inspect the working directory and pick the **first** match, from top to bottom:
1. **React Native**: `package.json` has a `react-native-purchases` entry, or `react-native` as a dependency → read `platforms/react-native.md`. If `expo` is also a dependency, note it as an Expo project.
2. **Flutter**: `pubspec.yaml` exists at the project root → read `platforms/flutter.md`.
3. **Kotlin Multiplatform**: `build.gradle.kts` contains a `kotlin { … }` multiplatform source sets block, or depends on `com.revenuecat.purchases:purchases-kmp*` → read `platforms/kmp.md`.
4. **Android (native)**: `build.gradle(.kts)` applies `com.android.application` (and is not KMP) → read `platforms/android.md`.
5. **iOS (native)**: `Package.swift`, `*.xcodeproj`, `*.xcworkspace`, or `Podfile` at the project root → read `platforms/ios.md`.
If several match (e.g. an `ios/` folder inside a Flutter project), pick the **outermost** project, the one that owns the build. If still ambiguous, ask the user which platform the bug reproduces on.
## 2. Universal code-side checklist
Walk these nine items in order. Most reports are resolved by steps 1 through 5.
1. **Turn on debug logging and reproduce.** The SDK narrates what it is doing. Roughly 80% of reports are diagnosable from the log output alone. Each platform file shows how to set `logLevel` to debug.
2. **Verify the API key platform matches the app.** iOS apps must use an `appl_…` public SDK key. Android apps must use `goog_…` (or `amzn_…` for Amazon). A mismatched key produces an authentication error on the first network call. On iOS this surfaces as an `INVALID_CREDENTIALS` error code. On Android it surfaces as `PurchasesErrorCode.InvalidCredentialsError`. Use the `list-app-public-api-keys` RevenueCat MCP tool to list the API keys for the project.
3. **Verify the bundle ID / package name matches the one set up in the RevenueCat project.**
List the apps using the `list-apps` RevenueCat MCP tool. The `bundle_id` (iOS / App Store) or `package_name` (Android / Play Store / Amazon Appstore) registered there must match the built app exactly, including capitalization. A mismatch causes offerings to come back empty because the app is not recognized.
4. **Verify offerings in the RevenueCat project.** List the project's offerings using the `list-offerings` RevenueCat MCP tool, passing the parameter `expand=items.package.product`. The offering marked with `is_current: true` must have at least one package attached, and each package must reference a store product. An offering with zero packages returns an empty `availablePackages` list even though `getOfferings` succeeds.
5. **Verify store products are live.** Products must be in "Ready to Submit" on App Store Connect or "Active" on Google Play Console. A product in a draft state will not be returned by the store, even in sandbox. If the SDK logs show offerings arriving from RevenueCat but products failing to resolve, this is almost always the cause. Use the `get-product-store-state` RevenueCat MCP tool to understand the state of product in App Store Connect or Google Play Console (field `store_status`). To fix or change the store state (not just read it), follow the `revenuecat-store-state` skill.
6. **Verify the testing account.** iOS: the device must be signed into a Sandbox Apple ID under Settings → App Store → Sandbox Account (set on iOS 14+ after the first sandbox prompt). Android: the tester's Gmail must be added to Google Play Console → Setup → License testing, and the app must be installed via the Internal Testing opt-in link, not sideloaded.
7. **Verify the network.** Corporate VPNs, captive portals, and some DNS filters silently block the RevenueCat API or the store APIs. Try a different network before digging deeper.
8. **Verify the appUserID.** If `logIn(appUserID)` was called with an ID that does not match what the user expects, entitlements appear missing because they are attached to a different RC user. Print `Purchases.shared.appUserID` (iOS) / `Purchases.sharedInstance.appUserID` (Android) and confirm it matches.
9. **Reset and retry.** Uninstall the app, re-sign into the sandbox / tester account, reinstall from the correct channel, relaunch.
## 3. Dashboard inspection via the RevenueCat MCP or the `rc` CLI
Use this when steps 3, 4, or 5 above point at dashboard configuration, when the user has no working app yet, or when you need to confirm a fix landed.
**Important:** The API key may have access to multiple projects. Always call `list-projects` first. If multiple projects are returned, ask the user which to inspect.
### Phase A: gather context
1. **Symptom** — "What specifically isn't working? What error messages are you seeing? Which platform (iOS, Android, Web)?"
2. **User state** — "Is this happening for new purchases or existing subscribers? Sandbox or production?"
### Phase B: systematic diagnosis
Work through this checklist via MCP tools or their `rc` CLI equivalents (see the `revenuecat-cli` skill). Each `list-*` tool maps to `rc <noun> list` (e.g. `list-offerings` → `rc offerings list`, `list-apps` → `rc apps list`), `list-app-public-api-keys` → `rc apps keys <app-id>`, and `get-product-store-state` → `rc products show <product-id> --store-state`.
#### Check 1: Project overview
```
list-projects → ask user to select project if multiple
list-apps (with selected project_id)
```
- Verify project exists and apps are present.
#### Check 2: Products
```
list-products
get-product-store-state
```
- [ ] Products exist for each store item.
- [ ] Store identifiers match App Store Connect / Play Console exactly.
- [ ] Product types are correct (subscription vs one-time).
- [ ] Play Store: using `product_id:base_plan_id` format.
- [ ] Store State: `store_status.status` = `ok`.
#### Check 3: Entitlements
```
list-entitlements
get-products-from-entitlement (for each entitlement)
```
- [ ] Entitlements exist for each access level.
- [ ] Products are attached to entitlements.
- [ ] No orphaned products (products not granting any entitlement).
#### Check 4: Offerings
```
list-offerings
list-packages
```
- [ ] At least one offering exists with `is_current: true`.
- [ ] Packages contain products.
- [ ] Package identifiers use standard conventions (`$rc_monthly`, etc.).
#### Check 5: Webhooks (if server-side issues suspected)
```
list-webhook-integrations
```
- [ ] Webhook URL is correct and accessible.
- [ ] Environment matches (production vs sandbox).
### Phase C: report and offer fixes
```
Diagnostic Report
=================
Project: {project_name}
Checks Passed: ✅
- Project exists and is accessible
- 2 apps configured (iOS, Android)
- 4 products found
Issues Found: ⚠️
1. CRITICAL: Product not attached to entitlement
Product: annual_premium (prod123)
Fix: Attach this product to an entitlement
2. WARNING: Offering has empty package
Offering: default / Package: $rc_annual has no products
Fix: Attach annual_premium to this package
3. INFO: No webhook configured
Optional but recommended for server-side access control
Recommended Actions:
1. Attach annual_premium to "premium" entitlement
2. Attach annual_premium to $rc_annual package
Would you like me to fix issues #1 and #2 now?
```
For each fixable issue, confirm with the user, then execute via MCP:
- `attach-products-to-entitlement`
- `attach-products-to-package`
## 4. Platform specific step
Read the platform file that matches detection. Each one lists platform specific gotchas not covered above (StoreKit configuration files, Gradle/desugaring, Metro caching, Expo prebuild, etc.).
- `platforms/ios.md`
- `platforms/android.md`
- `platforms/kmp.md`
- `platforms/flutter.md`
- `platforms/react-native.md`
## 5. Verify the fix
Do not declare the issue fixed until:
1. The log that previously showed the error now shows the expected success line (offerings returned with at least one package, purchase completed, entitlement active).
2. The dashboard reflects the change. For a purchase, check the Sandbox view on the Customers page and confirm the transaction is attached to the right `appUserID`.
3. The reproduction steps from the original report now pass.
If the user cannot reproduce locally, have them send the full debug log from app launch to the moment of failure. The SDK's own output is usually enough.
---
## Reference: SDK error codes
### Common errors
| Error code | Likely cause | Solution |
| -------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
| `INVALID_APP_USER_ID` | Reserved characters or empty string | Use alphanumeric IDs, underscores, hyphens only |
| `INVALID_CREDENTIALS` | Wrong API key or bundle ID mismatch | Verify API key matches app |
| `NETWORK_ERROR` | No connectivity or firewall | Check network, verify RevenueCat domains allowed |
| `STORE_PROBLEM` | Store downtime, config issue, iOS 18.x bug | Check store status, verify config, see Known iOS Issues below |
| `SIGNATURE_VERIFICATION_FAILED` | Tampered receipt or config error | Verify In-App Purchase Key (iOS) or service credentials |
### Purchase errors
| Error code | Solution |
| ------------------------------------- | ----------------------------------------------------------------- |
| `RECEIPT_ALREADY_IN_USE` | Call `restorePurchases()` or sync customer |
| `PRODUCT_NOT_AVAILABLE_FOR_PURCHASE` | Verify product status in App Store Connect / Play Console |
| `PURCHASE_NOT_ALLOWED` | Check parental controls, payment method |
| `PRODUCT_ALREADY_PURCHASED` | Call `restorePurchases()` to sync |
## Reference: debug log interpretation
Ask the developer to enable debug logging:
- iOS: `Purchases.logLevel = .debug`
- Android: `Purchases.logLevel = LogLevel.DEBUG`
Log emoji indicators: 🍎 Apple/StoreKit · 🤖 Google Play · 📦 Amazon · 😿 RevenueCat backend.
## Reference: known platform issues
### iOS
**iOS 18.0–18.3.2: StoreKit Daemon Connection Failure**
- Symptom: `STORE_PROBLEM` (NSCocoaErrorDomain Code 4097) on ~25% of purchases on physical devices.
- Fix: Upgrade to iOS 18.4+.
**iOS 18.4–18.5 Simulator: Products Don't Load**
- Symptom: Products return empty in simulator with sandbox.
- Affected: Simulator only — physical devices and production unaffected.
- Fix: Test on physical device, or use Xcode 26+ with iOS 26+ simulators.
### Android
**ProxyBillingActivity NullPointerException**
- Typically from automated testing or Play Store pre-launch reports on LG Nexus 5X / rooted devices.
- Safe to ignore/silence in crash reporting tools.
**NoCoreLibraryDesugaringException / NoClassDefFoundError**
- Fix: Enable core library desugaring in `build.gradle` or raise `minSdk`.
## Reference: platform configuration checklists
### iOS
- [ ] Paid Applications agreement signed in App Store Connect.
- [ ] In-App Purchase Key uploaded to RevenueCat (StoreKit 2 / SDK 5.x+).
- [ ] Products show "Ready to Submit" or "Approved" status.
- [ ] Bundle ID matches exactly in Xcode, App Store Connect, and RevenueCat.
- [ ] New products: wait 24h for propagation.
### Android
- [ ] App published to at least closed testing track (internal testing won't work).
- [ ] Test account added as licensed tester in Play Console.
- [ ] Service account credentials (JSON) uploaded to RevenueCat with Finance permissions.
- [ ] Subscriptions use `product_id:base_plan_id` format.
- [ ] New products: wait 24h for propagation.
## Reference: App Store rejection troubleshooting
**"Issues fetching products"** — Products must be submitted for review with the app on first submission. Create products in App Store Connect, then submit app and products together.
**"Error during purchase" (Sandbox)** — Apple sandbox downtime. Inform reviewer, provide RevenueCat sandbox dashboard screenshot showing test purchases work, ask to retry.
**"Content not unlocked after purchase"** — Verify product → entitlement connection in RevenueCat. Ensure app calls `getCustomerInfo()` after purchase.
## Reference: common issues
**User purchased but has no entitlement** — Check product → entitlement attachment and verify store identifier matches exactly.
**Offering returns empty** — Verify a `current` offering exists, packages have products attached, and products exist in the app's store.
**Webhook not receiving events** — Verify URL is internet-accessible and returns 200 OK. Test with webhook.site.
**Subscription status out of sync** — SDK caches `CustomerInfo` for 5 min (foreground). Force refresh:
```swift
// iOS
Purchases.shared.getCustomerInfo(fetchPolicy: .fetchCurrent) { ... }
```
```kotlin
// Android
Purchases.sharedInstance.getCustomerInfoWith(CacheFetchPolicy.FETCH_CURRENT) { ... }
```
**SDK crashes on launch (iOS / Xcode 26)** — Initialize RevenueCat before other networking libraries.
**SDK crashes on launch (Android)** — Enable core library desugaring or raise `minSdk` to 24+.
Referenced files: 5
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package author
- RevenueCat
Package observed Oct 2, 2026.
Technical details
- First seen
- Sep 30, 2026 · 22:02 UTC
- Last seen
- Oct 2, 2026 · 12:00 UTC
- Collection status
- Collected
plugin_asdk_app_6a69853c7df08191801acfc8ff769b01
Download plugin data (JSON)