@tollahq/sdk
v0.2.0
Published
Free-tier economics for AI and usage-based SaaS: see what each free user costs and which ones convert.
Maintainers
Readme
@tollahq/sdk
See what each free user of your AI product costs, which ones convert, and which accounts are the same person. This is the server SDK for Tolla.
npm install @tollahq/sdkimport OpenAI from "openai";
import { Tolla, withUser, wrapOpenAI } from "@tollahq/sdk";
const tolla = new Tolla(); // reads TOLLA_SECRET_KEY
const openai = wrapOpenAI(new OpenAI(), tolla);
// 1. When someone signs up
tolla.identify({ userId: user.id, createdAt: user.createdAt, identity: { email: user.email, ip: req.ip } });
// 2. Every AI call: cost is recorded per user automatically
await withUser(user.id, () => openai.chat.completions.create({ model: "gpt-4o", messages }));Payments come from Stripe: connect it with a read-only key on your Tolla Connect page. No code needed.
Safe by design
- Never blocks, never throws. Calls return immediately; events are sent in the background in batches.
- Survives outages. Retries with backoff and a timeout; if Tolla stays unreachable, events are dropped with a warning (bounded queue,
maxQueueSize), and your product carries on. - Retry-safe. Every event gets an id, so a resend is never counted twice.
- No prompts, no outputs. The AI wrappers read only
modelandusagefrom responses. - Server only. A secret key in a browser is refused; use the browser snippet with your publishable key there.
AI cost
wrapOpenAI and wrapAnthropic record model and tokens for every billable call (chat, responses, embeddings, messages; streaming included). Tolla prices the tokens server-side from a maintained price list, including cached-input and cache-write rates, so new prices never need an SDK update. You can override any model's price in your workspace.
Attach the user with withUser(userId, fn) (or withUser({ userId, feature }, fn)), or per client: wrapOpenAI(client, tolla, { userId, feature: "chat" }).
Streaming with OpenAI Chat Completions: usage is only sent when you set stream_options: { include_usage: true }. The Responses API and Anthropic always include it.
Other providers: send the tokens yourself.
tolla.cost({ userId, feature: "chat", model: "gemini-2.5-pro", inputTokens, outputTokens });
// or a cost you computed: tolla.cost({ userId, feature: "render", costUsd: 0.04 })Changes
Changes you set live in Tolla (a cap on free AI, a first-day limit, a card for repeat accounts) can be applied by the wrapper, with no new code:
const openai = wrapOpenAI(new OpenAI(), tolla, { changes: true });
try {
await withUser(user.id, () => openai.chat.completions.create({ … }));
} catch (e) {
if (e instanceof TollaChangeError) return showUpgrade(e.message); // e.action: "limit" | "verify_card"
throw e;
}Before a free user's call the wrapper asks Tolla (tolla.decide(userId), cached for a minute). It waits 400 ms at most: if Tolla is slow or unreachable, the call goes ahead unchanged. Paying users and the change's control group are never limited. With changes on, wrapped calls return a plain Promise (helpers such as .withResponse() are not available on them). You can also call await tolla.decide(userId) yourself, for example to show a one-time extension (action: "extend").
Per-user allowances
When a per-user allowance is live in Tolla, ask for a person's free tier where you set up their credits and trial:
const a = await tolla.allowance(user.id); // null: keep today's free tier
if (a?.cardFirst) return askForCard(a.message);
const credits = Math.round(FREE_CREDITS * (a?.share ?? 1)); // e.g. 1.1 = 10% more, 0.5 = half
const trialDays = TRIAL_DAYS + (a?.trialDays ?? 0); // 0 = no new trial for this groupa.reason is a plain sentence you can show the person. Heavy users get a.noticeUntil a week before their smaller tier applies.
Serverless
Background sending needs the process to stay alive. In Vercel functions, Lambda and similar, flush before the response ends:
import { after } from "next/server";
after(() => tolla.flush());Options
| Option | Default | |
|---|---|---|
| apiKey | process.env.TOLLA_SECRET_KEY | Secret key (tolla_sk_…). Without one the SDK does nothing. |
| baseUrl | https://tolla.vercel.app | Or TOLLA_BASE_URL. |
| batchSize | 100 | Send when this many events are queued (max 500). |
| flushIntervalMs | 1000 | Send at least this often. |
| maxQueueSize | 10 000 | Events kept while Tolla is unreachable. |
| timeoutMs | 10 000 | Per request. |
| maxRetries | 5 | For network errors, 429 and 5xx. |
| onError | console warning, at most once a minute | Receives every problem, including events Tolla rejected and why. |
| disabled | false | Turn into a no-op (tests). |
The Connect page in Tolla has a live event debugger: every event received, and what was wrong with the ones that were not accepted.
MIT licensed.
