Docs

Setup takes about ten minutes: create an app, report AI usage from your backend, and connect RevenueCat.

Quickstart

  1. Sign in and create an app. Copy the ingest key (shown once) and store it on your backend as FISCALLM_KEY.
  2. Install the SDK on your backend and track each AI call (below).
  3. Add the webhook URL from the app's Setup page to RevenueCat (below).

The Setup page shows when the first event and the first webhook arrive, and how many of your AI users it could match to RevenueCat.

Set up with an AI assistant

The FiscaLLM setup skill tells a coding assistant how to find your AI calls, add the SDK, pass the RevenueCat user ID through and leave the keys to you. In Claude Code, install it in your project and ask it to "set up FiscaLLM":

mkdir -p .claude/skills/fiscallm-setup && curl -fsSL https://fiscallm.com/skill.md -o .claude/skills/fiscallm-setup/SKILL.md

In Cursor, Copilot or another assistant, paste:

Read https://fiscallm.com/skill.md and follow it to set up FiscaLLM in this project.

The SDK

npm install fiscallm

Zero dependencies. Runs on Node 20+, Cloudflare Workers, Vercel, Supabase Edge Functions (Deno), Bun and Firebase. It never throws, never blocks your response, and sends only token counts: prompts and responses never leave your backend.

import { createMeter } from "fiscallm";

const meter = createMeter({
  apiKey: env.FISCALLM_KEY,
  waitUntil, // optional, see "Serverless platforms"
});

// userId: the user's RevenueCat app user ID
const res = await openai.chat.completions.create({ model, messages });
meter.trackOpenAI(res, { userId, feature: "chat" });

Providers

CallPass
meter.trackOpenAI(res, ctx)A Chat Completions or Responses API result, from the SDK or await r.json()
meter.trackAnthropic(res, ctx)A Messages API result
meter.trackGemini(res, ctx)A generateContent result, from @google/genai or REST
meter.track(event)Anything else: tokens, or an exact costUsd for images and voice

ctx is { userId, feature?, provider?, model? }. feature is your own label ("chat", "summary") to break cost down by. For OpenAI-compatible APIs such as Groq, OpenRouter or DeepSeek, use trackOpenAI(res, { userId, provider: "groq" }).

meter.track({
  userId,
  provider: "fal",
  model: "flux-pro",
  costUsd: 0.05,      // for calls not priced per token
  feature: "avatar",
});

Streaming

Wrap the stream; chunks pass through unchanged and usage is recorded when it ends. OpenAI Chat Completions only reports usage with stream_options: { include_usage: true }.

const stream = meter.trackStream(
  await openai.chat.completions.create({
    model, messages, stream: true,
    stream_options: { include_usage: true },
  }),
  { userId },
);
for await (const chunk of stream) { /* ... */ }

If your backend proxies a provider's raw server-sent events straight to the app, wrap the response instead:

const upstream = await fetch("https://api.anthropic.com/v1/messages", init);
return meter.trackSSE(upstream, { userId });

Serverless platforms

Events are batched and sent in the background. On serverless platforms, tell the SDK how to keep the function alive until they are sent:

Cloudflare WorkerscreateMeter({ apiKey, waitUntil: ctx.waitUntil.bind(ctx) })
Vercelimport { waitUntil } from "@vercel/functions", then pass waitUntil
Supabase Edge FunctionswaitUntil: (p) => EdgeRuntime.waitUntil(p)
Firebase, AWS Lambdaawait meter.flush() before returning
Long-running Node serverNothing: events are sent every two seconds

Each request has a two-second timeout and one retry. If FiscaLLM is unreachable, events are dropped and your app is unaffected.

Python

pip install fiscallm

Same helpers and guarantees as the TypeScript SDK, with no dependencies, on Python 3.9+. Tracking only queues; a background thread sends.

import os
from fiscallm import Meter

meter = Meter(api_key=os.environ["FISCALLM_KEY"])  # one per process

res = client.chat.completions.create(model=model, messages=messages)
meter.track_openai(res, user_id=user_id, feature="chat")
# also: track_anthropic, track_gemini, track(...)

# Streams, sync or async: iterate the returned stream
stream = meter.track_stream(response_stream, user_id=user_id)

# AWS Lambda, Cloud Functions: before returning
meter.flush()   # or: await meter.aflush()

User IDs

The userId you send must be the user's RevenueCat app user ID. That is how AI cost is joined to revenue.

Users with AI cost but no RevenueCat record appear as "free or unmatched". Free users never reach RevenueCat, so some unmatched cost is expected. A low match rate on the Setup page usually means the IDs differ.

RevenueCat

  1. In RevenueCat: Project → Integrations → Webhooks → Create new webhook.
  2. Webhook URL and Authorization header value: copy both from your app's Setup page.
  3. Environment: Production. To test with sandbox or Test Store purchases, choose both and turn on test mode in Settings.

Revenue is net: price × (1 − tax − store commission), as RevenueCat reports them. Refunds subtract on the day they happen.

RevenueCat sends only new events. To bring in subscribers who paid before you connected, use Settings → Import past subscribers: paste your RevenueCat project ID and a v2 secret key with read-only access to customer information, and FiscaLLM imports their purchases, renewals and trials from RevenueCat's API. Purchases the webhook already delivered are skipped, so nothing counts twice. The key is stored encrypted only while the import runs.

Prices

Cost is calculated on our side from the providers' public list prices, including cached tokens, cache writes and long-prompt pricing, and the price that applied on the day of each call. If you pay less, add a discount for a provider or your own prices for a model under Settings → Your AI prices. Models without a price are listed there too; their cost counts as zero until you add one or send costUsd.

Ingest API

For backends not written in TypeScript, send events directly:

POST https://fiscallm.com/api/v1/events
Authorization: Bearer <FISCALLM_KEY>
Content-Type: application/json

{
  "events": [{
    "id": "9b2c6f0e-3f7c-4d8a-9a51-2f0a4f1c7e21",   // a UUID you generate; retries reuse it
    "userId": "rc-app-user-id",
    "ts": "2026-10-17T12:00:00Z",
    "provider": "openai",
    "model": "gpt-5-mini",
    "inputTokens": 1200,       // excluding cache reads and writes
    "outputTokens": 300,
    "cachedTokens": 0,
    "cacheWriteTokens": 0,
    "costUsd": 0.0012,         // optional: overrides our price
    "feature": "chat"          // optional
  }]
}

Up to 100 events per request. The response is 202 with { accepted, duplicates, unpriced }. Events with anid already received are counted as duplicates and ignored, so retrying is safe. Timestamps may be up to 90 days old.

Data and retention