# Integrating Tolla

Tolla shows an AI product what each free user costs, which users convert, and which accounts belong to the same person. Measuring only observes: it never changes what users see. Changes the user tries later in Tolla are switched on separately, never by this guide.

This guide is written for coding agents and developers. Follow it in order. There are three touchpoints, all on the server.

## Rules

- Read keys from the environment: `TOLLA_SECRET_KEY` and `TOLLA_BASE_URL=https://gettolla.com`. Never write a key into code, logs or commits. The user sets them; if they are missing, ask the user to add them (they come from the Settings page in Tolla).
- Never send prompts, completions or file contents to Tolla. Only user ids, emails, IPs, model names and token counts.
- The SDK never throws and never blocks, so no try/catch is needed around its calls, and its calls do not need to be awaited (except `flush()` in serverless functions).
- Do not change product behaviour. Work on a new branch and summarize every change at the end.
- When done, tell the user to open the Settings page in Tolla: its "Latest events" list shows each event as it arrives, and why one was refused.

## Environment

```bash
TOLLA_SECRET_KEY=tolla_sk_…
TOLLA_BASE_URL=https://gettolla.com
```

## 1. Install the SDK

- Node.js / TypeScript: `npm install @tollahq/sdk`
- Python: `pip install tolla`
- Anything else: use the HTTP API in section 5.

Create one shared client and import it everywhere:

```ts
// lib/tolla.ts — one shared client
import { Tolla } from "@tollahq/sdk";
export const tolla = new Tolla(); // reads TOLLA_SECRET_KEY (and TOLLA_BASE_URL)
```

```python
# tolla_client.py — one shared client
from tolla import Tolla
tolla = Tolla()  # reads TOLLA_SECRET_KEY (and TOLLA_BASE_URL)
```

## 2. Identify users when they sign up

Find the server-side code that creates a new user account (sign-up handler, auth callback such as Clerk/Auth0/Supabase webhooks or NextAuth `events.createUser`). Right after the account exists, identify the user. Also call it again when the user gets a Stripe customer id.

```ts
// where a new account is created, on your server
tolla.identify({
  userId: user.id,
  createdAt: user.createdAt,
  stripeCustomerId: user.stripeCustomerId, // if you have one
  identity: { email: user.email, ip: request.ip },
});
```

```python
# where a new account is created
tolla.identify(
    user.id,
    created_at=user.created_at,
    stripe_customer_id=user.stripe_customer_id,  # if you have one
    identity={"email": user.email, "ip": request.remote_addr},
)
```

`userId` must be the same id the rest of the app uses for that user.

## 3. Record the cost of every AI call

Find where the AI client is created and wrap it once. Then make sure each request that calls the model runs with the user attached.

**OpenAI or Anthropic SDK (Node):**

```ts
import OpenAI from "openai";          // or Anthropic from "@anthropic-ai/sdk"
import { withUser, wrapOpenAI } from "@tollahq/sdk"; // or wrapAnthropic
import { tolla } from "@/lib/tolla";

const openai = wrapOpenAI(new OpenAI(), tolla);

// in a request handler that knows the user:
await withUser({ userId: user.id, feature: "chat" }, () =>
  openai.chat.completions.create({ model: "gpt-4o", messages }),
);
```

**OpenAI or Anthropic SDK (Python):**

```python
from openai import OpenAI            # or anthropic.Anthropic
from tolla import with_user, wrap_openai  # or wrap_anthropic
from tolla_client import tolla

openai = wrap_openai(OpenAI(), tolla)

# in a request handler that knows the user:
with with_user(user.id, feature="chat"):
    openai.chat.completions.create(model="gpt-4o", messages=messages)
```

For OpenAI Chat Completions with `stream: true`, add `stream_options: { include_usage: true }` so usage is reported.

**Vercel AI SDK:** either use OpenTelemetry (section 4) or record usage after each call:

```ts
// Vercel AI SDK, without OpenTelemetry: record usage after each call
const result = await generateText({ model, prompt });
tolla.cost({
  userId: user.id,
  feature: "chat",
  model: result.response.modelId,
  inputTokens: result.usage.inputTokens ?? 0,
  outputTokens: result.usage.outputTokens ?? 0,
  cachedInputTokens: result.usage.inputTokenDetails?.cacheReadTokens, // AI SDK 6; v5: result.usage.cachedInputTokens
  cacheWriteInputTokens: result.usage.inputTokenDetails?.cacheWriteTokens,
});
```

**Other providers:** call `tolla.cost({ userId, feature, model, inputTokens, outputTokens })` with the provider's usage numbers. Tolla prices tokens itself.

**Images, video or audio** (Replicate, fal, OpenAI Images, Runway, ElevenLabs and the like): there are no tokens, so after each generation record what it cost. If the app knows the price, send it:

```ts
tolla.cost({ userId: user.id, feature: "image_generation", model: "flux-1.1-pro", costUsd: 0.04 });
```

```python
tolla.cost(user.id, feature="image_generation", model="flux-1.1-pro", cost_usd=0.04)
```

Otherwise send the count (`quantity`: images, or seconds of video or audio) without a cost, and ask the user to set the price per unit for that feature under Feature prices on the Settings page; Tolla multiplies the two.

**Serverless (Vercel functions, Lambda):** flush before the function ends.

```ts
// Vercel / serverless: send queued events before the function ends
import { after } from "next/server";
after(() => tolla.flush());
```

In Python serverless handlers call `tolla.flush()` before returning.

## 4. Or: OpenTelemetry, with no SDK

If the app already exports OpenTelemetry traces with GenAI attributes (Vercel AI SDK telemetry, OpenLLMetry, LangChain, LlamaIndex), point the trace exporter at Tolla:

```bash
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://gettolla.com/api/otel/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer ${TOLLA_SECRET_KEY}
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json
```

```ts
// Vercel AI SDK: turn on telemetry and say who the user is
await generateText({
  model,
  prompt,
  experimental_telemetry: { isEnabled: true, functionId: "chat", metadata: { userId: user.id } },
});
```

```python
# Any OpenTelemetry GenAI instrumentation (OpenLLMetry, LangChain, LlamaIndex…):
# set the span attribute user.id on your request span or AI spans, e.g. in Python:
from opentelemetry import trace
trace.get_current_span().set_attribute("user.id", user.id)
```

Only model, token counts and the user id are read from spans; one cost is recorded per model call.

## 5. HTTP API (any language)

`POST https://gettolla.com/api/v1/batch` with up to 500 items. Kinds: `identify`, `track`, `cost`, `revenue`. Each item is validated on its own; the response lists rejected items by index with the reason. Send an `eventId` so retries are not counted twice. Send `revenue` only for payments that don't go through a connected Stripe account; Stripe payments are already synced, and sending them again counts them twice.

```bash
curl https://gettolla.com/api/v1/batch \
  -H "Authorization: Bearer $TOLLA_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items": [
    {"kind": "identify", "data": {"userId": "u_123", "createdAt": "2026-10-05T09:00:00Z",
                                  "identity": {"email": "ada@example.com", "ip": "203.0.113.7"}}},
    {"kind": "cost", "data": {"userId": "u_123", "feature": "chat", "model": "gpt-4o",
                              "inputTokens": 812, "outputTokens": 230, "eventId": "req_9f2"}}
  ]}'
```

## Optional: product events

Moments that show upgrade intent make the conversion analysis sharper. Track them where they happen, if they exist in the app: `pricing_viewed`, `limit_reached`, `checkout_started`, `payment_method_added`, `trial_started`.

```ts
tolla.track({ userId: user.id, event: "limit_reached", metadata: { limit: "daily_images" } });
```

## Payments

Payments come from Stripe and need no code: the user connects a read-only Stripe key on the Settings page.

## Optional: browser snippet

A small script adds a first-party device id, which links duplicate free accounts that use different emails. It needs the publishable key and a server-signed user id (`userHash`). Ask the user before adding it, and follow these rules:

- Load it only on the page shown right after a sign-up or a sign-in, and call `tolla.identify()` there. It reads and stores nothing until identify() runs, and its only purpose is recognising devices that create several free accounts, a security measure. Never use it for analytics or marketing.
- The site's cookie notice should list it under necessary or security cookies: `_tolla_id` (random device id, up to 2 years) and `_tolla_sid` (session storage).
- If the user wants to wait for consent (cautious EU sites): add `data-consent="wait"` to the script tag and call `tolla.consent()` when the visitor accepts.
- The optional device fingerprint and automation checks (off by default, switched on per workspace) need the visitor's consent in the EU.

Instructions are also on the Settings page.

## Deleting a user

When one of the app's users deletes their account or asks to be forgotten, delete what Tolla stores about them too. With the secret key, from the server:

```bash
curl -X DELETE "$TOLLA_BASE_URL/api/v1/users/<userId>" -H "Authorization: Bearer $TOLLA_SECRET_KEY"
```

`<userId>` is the same id passed to identify(). The answer is `{ "ok": true }`, or 404 if Tolla has no such user. If the app already has an account-deletion handler, add this call there.
