@reveny/node
v0.2.0
Published
Reveny SDK — AI cost tracking plus subscription, payment and usage reporting for real unit economics (MRR, churn, margin).
Maintainers
Readme
@reveny/node
Drop-in AI cost tracking with per-user, per-tenant, per-feature attribution. Pluggable into OpenAI, Anthropic, Vercel AI SDK, ElevenLabs, fal.ai, Replicate, or any HTTP-based LLM provider.
Reveny answers "which user / tenant / feature is bleeding my runway?" — not just "what did I spend?".
Install
pnpm add @reveny/nodeThe four-tier integration model
Pick the level that fits your stack. They compose — most apps use Tier 3 + Tier 4 together.
| Tier | What it covers | Fidelity | Setup cost |
|---|---|---|---|
| 1. autoTrack | Any provider with HTTP API. Patches global fetch. | Medium — captures cost, no feature / tenantId unless context is set | One line |
| 2. AI SDK middleware | All ~20 providers Vercel AI SDK supports | High when AI SDK exposes the data | One middleware |
| 3. Per-provider wrappers | OpenAI / Anthropic SDKs directly | Highest — cache tokens, streaming, full nuance | One Proxy wrap per client |
| 4. trackCall + curated helpers | ElevenLabs, fal, Replicate, custom HTTP, anything else | High when extractor matches reality | Per call site |
| last resort | reveny.track({...}) manual | Whatever you compute yourself | Free-form |
Tier 1 — autoTrack (safety net)
import { autoTrack } from '@reveny/node';
autoTrack({ apiKey: process.env.REVENY_KEY!, projectId: process.env.REVENY_PROJECT_ID! });Done. Any fetch to api.openai.com, api.anthropic.com, generativelanguage.googleapis.com, api.mistral.ai, api.groq.com, api.together.xyz, api.perplexity.ai, api.deepseek.com, openrouter.ai, api.x.ai, api.cohere.ai, or your Azure OpenAI is captured automatically.
Pair with revenyContext.run({ userId, tenantId, feature }) blocks for attribution. Without context, events are tracked but unattributed — useful for total cost, not for unit economics.
Streaming responses (SSE) are skipped by default. Disable by env or upgrade to a wrapper.
Tier 2 — Vercel AI SDK middleware
import { wrapLanguageModel } from 'ai';
import { trackerMiddleware } from '@reveny/node/ai-sdk';
const model = wrapLanguageModel({
model: openai('gpt-4o'),
middleware: trackerMiddleware({
apiKey: process.env.REVENY_KEY!,
projectId: process.env.REVENY_PROJECT_ID!,
}),
});Covers OpenAI, Anthropic, Google, Mistral, Cohere, Bedrock, Azure, Groq, Together, Perplexity, DeepInfra, DeepSeek, xAI, and more — anything the AI SDK abstracts.
Tier 3 — Per-provider wrappers (highest fidelity)
import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import { withTracker as wrapOpenAI } from '@reveny/node/openai';
import { withTracker as wrapAnthropic } from '@reveny/node/anthropic';
const config = {
apiKey: process.env.REVENY_KEY!,
projectId: process.env.REVENY_PROJECT_ID!,
};
export const openai = wrapOpenAI(new OpenAI(), config);
export const anthropic = wrapAnthropic(new Anthropic(), config);Captures cache tokens, stream usage chunks, and provider-specific details. Use for your most-trafficked LLM calls.
Tier 4 — trackCall + curated helpers
For media APIs and providers without dedicated wrappers.
Curated (no extractor needed):
import { trackTts } from '@reveny/node/elevenlabs';
import { trackImageGen } from '@reveny/node/fal';
import { trackPrediction } from '@reveny/node/replicate';
const audio = await trackTts(reveny, () => elevenLabs.textToSpeech.convert(voiceId, { text }), {
model: 'eleven_multilingual_v2',
text,
});
const img = await trackImageGen(reveny, () => fal.subscribe('veed/fabric-1.0', { input }), {
model: 'veed/fabric-1.0',
width: 1024,
height: 1024,
});
const pred = await trackPrediction(reveny, () => replicate.run('bytedance/latentsync:abc', { input }), {
model: 'bytedance/latentsync',
gpuType: 'a100',
});Generic (you write the extractor):
const result = await reveny.trackCall(
() => myCustomLlmCall(input),
{
provider: 'custom',
model: 'my-model-v1',
extractUsage: (res) => ({ inputTokens: res.tokens.in, outputTokens: res.tokens.out }),
},
);Attribution: userId, tenantId, feature
Wrap any block of work in revenyContext.run and every event tracked from inside inherits the context:
import { revenyContext } from '@reveny/node';
await revenyContext.run(
{ userId: req.user.id, tenantId: req.user.tenantId, feature: 'scene_suggestion' },
() => anthropic.messages.create({ model: 'claude-haiku-4-5', messages: [...], metadata: { user_id: req.user.id } }),
);Compatible with Promise.all — each scope is isolated. Or use reveny.withContext(...) if you have a Reveny instance handy.
identify — give your dashboard real names
Once at login (or whenever you know who the user is):
await reveny.identify(user.id, {
email: user.email,
tenantId: user.tenantId,
});The dashboard shows emails and groups by tenant instead of opaque user IDs.
Unit economics — revenue, MRR & margin
Reveny is payment-gateway agnostic: you report revenue through the SDK, not through a
Stripe/Paddle/RevenueCat integration. Call these from your webhook handlers or backend whenever
state changes. All money is in micros (1 USD = 1_000_000 micros). Every method is async and
never throws into your app.
// Subscription state — idempotent. Reveny computes New / Expansion / Contraction / Churn for you.
await reveny.subscriptions.set(user.id, {
status: 'active', // 'trialing' | 'active' | 'past_due' | 'canceled' | 'paused'
mrrMicros: 1_900_000, // $19.00, monthly-normalized (annual → divide by 12)
currency: 'USD',
planId: 'pro',
});
// Payments — for payment success/failure analytics.
await reveny.payments.record(user.id, {
amountMicros: 1_900_000,
status: 'succeeded', // 'succeeded' | 'failed' | 'refunded'
invoiceId: 'inv_123',
});
// Per-SKU costs that aren't from a tracked AI provider (needs a server-side cost rate).
await reveny.usage.record(user.id, { sku: 'whatsapp_message', units: 1 });
// Subscriber traits for the dashboard (name, acquisition source, cohort).
await reveny.subscribers.identify(user.id, {
email: user.email,
acquisitionSource: 'organic',
});Wiring from a Stripe webhook, for example:
// customer.subscription.updated
await reveny.subscriptions.set(sub.metadata.appUserId, {
status: mapStripeStatus(sub.status),
mrrMicros: normalizeToMonthlyMicros(sub),
currency: 'MXN',
planId: sub.items.data[0]?.price.id,
eventId: stripeEvent.id, // dedupe webhook redeliveries
});Idempotency. Pass eventId with a stable id from your source system (e.g. the Stripe
event id) so a redelivered webhook is deduplicated server-side and a payment is never counted
twice. If you omit it, a random id is generated — which only dedupes the SDK's own network
retries, not caller-level retries. For subscriptions.set, re-sending identical state is also a
no-op. subscriberId is the same identifier as userId; inside a revenyContext.run({ userId })
scope you can pass it implicitly.
What ships in the box
@reveny/node—Reveny,autoTrack,revenyContext,trackCall, plus the unit-economics surface (reveny.subscriptions,reveny.payments,reveny.usage,reveny.subscribers)@reveny/node/openai— OpenAI wrapper@reveny/node/anthropic— Anthropic wrapper@reveny/node/ai-sdk— Vercel AI SDK middleware@reveny/node/elevenlabs—trackTts,trackVoiceDesign@reveny/node/fal—trackImageGen@reveny/node/replicate—trackPrediction
License
MIT
