In short: AI margin per user is what a user paid you, net of store fees and tax, minus what their AI usage cost. The hard part isn't the arithmetic. It's using the same user ID in your AI tracking and in RevenueCat, and counting revenue the way it really arrives.
RevenueCat tells you who paid. Your AI provider tells you how many tokens you used. Neither knows about the other, so margin per user has to be built by joining them.
Every join below depends on one value: the RevenueCat app user ID. Your backend must report each AI call under that exact ID.
Best option: log users into RevenueCat with your own user ID (Purchases.logIn(yourUserId)). Then the RevenueCat ID is the ID your backend already has from your own auth, and nothing extra needs to travel.
If you use RevenueCat's anonymous IDs (they start with $RCAnonymousID:), your backend doesn't know the value. Send it from the app with each AI request, for example in a header:
| Platform | Get the app user ID |
|---|---|
| iOS (Swift) | Purchases.shared.appUserID |
| Android (Kotlin) | Purchases.sharedInstance.appUserID |
| React Native | await Purchases.getAppUserID() |
| Flutter | await Purchases.appUserID |
// backend: use the ID you were sent (or better, the one tied to your auth)
const userId = req.headers["x-rc-app-user-id"];A header sent by the app can be faked, so for money decisions prefer an ID you derive from your own authenticated session. For measuring margin, a header is fine.
Aliases: when a user goes from anonymous to logged in, RevenueCat merges the IDs and lists the old ones as aliases. Your join should treat all of a user's aliases as one person, otherwise their cost and their revenue land on different IDs.
Add a webhook (Integrations → Webhooks) that points at your own endpoint. Each event describes one thing that happened to a subscription. The ones that matter for revenue:
| Event | What it means for margin |
|---|---|
INITIAL_PURCHASE (trial) | A trial starts: AI cost begins, revenue is $0 |
INITIAL_PURCHASE (paid) | First payment: add net revenue |
RENEWAL | Another payment: add net revenue. A trial conversion is marked on the first one |
CANCELLATION | Auto-renew turned off, or a refund (negative price): subtract refunds |
EXPIRATION | Access ended: no more revenue, but cost may continue if you don't cut them off |
PRODUCT_CHANGE | Plan changed: attribute future cost to the new plan |
Two practical rules: de-duplicate on the event ID (RevenueCat retries deliveries), and ignore sandbox events in production reporting.
The App Store and Google Play keep a cut, and tax is collected on top. Each event carries the price and both percentages, so:
net_revenue = price_usd × (1 − tax_percentage − commission_percentage)On a $9.99 plan a 15% store cut and some tax can leave you with well under $8. Measuring margin against $9.99 overstates it for every user.
margin = net_revenue − ai_cost (same user, same time window)FiscaLLM does the join for you. One line reports AI cost under the user's ID, a RevenueCat webhook reports payments, and you get margin per user, per plan and per trial cohort, with alerts when someone crosses a budget. Aliases, net revenue, de-duplication and refunds are handled. Annual plans are counted when the payment arrives, so use a 90-day view for them.
meter.trackOpenAI(res, { userId, feature: "chat" }); // userId = RevenueCat app user ID
// RevenueCat → Integrations → Webhooks → paste the URL and authorization value from your Setup pageIt's free and open source. Open the demo dashboard (no sign-up), read how it works, or start free. Per-provider details: OpenAI and Anthropic Claude.
No. It reports revenue and subscription state only, so AI cost has to come from your own backend and be joined by user ID.
The same method works with any billing system: report each payment, refund and cancellation under the same user ID your AI calls use.
The RevenueCat app user ID. If you log users in with your own ID, that is your own ID.