TrackDocs

Developer docs

01 // Overview

Overview

ve-track is cost attribution for AI shaped apps. One install, one wrapper line, and every provider fetch your app makes is priced and attributed to your app, your Clerk org, your end user, and the action they ran. It shows up on your dashboard within seconds.

OpenAIAnthropicGeminiPerplexityOpenRouterCloroFalZyteDataForSEOApifyFirecrawlBrightDataCloudflare

The fastest path is the Quickstart. Already integrated and curious what changed? Pricing and version history live in the CHANGELOG.

02 // Quickstart

Quickstart

Four steps, about a minute. No infra to spin up.

1. Install

Add the package to any Cloudflare Worker, Node, Bun, or Deno project.

bash
bun add github:Handbook-Enterprises/ve-track
# or: npm install github:Handbook-Enterprises/ve-track

2. Issue a key

Open the Keys page, click New key, and copy the vt_live_… value.

3. Add two env vars

Put these in your worker's .dev.vars, and as secrets in production.

.dev.vars
VE_TRACK_KEY=vt_live_xxxxxxxxxxxxxxxxxxxxxxxx
VE_TRACK_BASE_URL=https://track.viewengine.ai

4. Wrap your handler

worker.ts
import { trackHandler } from "@viewengine/track";
import app from "./api";

export default trackHandler<Env>(
  { app: "my-app" },
  {
    fetch: (req, env, ctx) => app.fetch(req, env, ctx),
  },
);

That is it. Every external provider fetch is now intercepted, priced, attributed to the signed in Clerk user and org, and shipped to your dashboard. No Clerk app? Pass resolveUser: "none" to skip user attribution.

03 // Worker shapes

Worker shapes

trackHandler wraps any worker entry point. Pick the shape that matches yours.

Plain HTTP

ts
export default trackHandler<Env>(
  { app: "my-app" },
  { fetch: (req, env, ctx) => app.fetch(req, env, ctx) },
);

Queue

Wrap each message with trackMessage. The producer stamps auth and action on the message body, and ve-track reads them automatically.

ts
export default trackHandler<Env>(
  { app: "my-app" },
  {
    queue: async (batch, env, ctx) => {
      await Promise.all(
        batch.messages.map((message) =>
          trackMessage(message, async () => {
            await processOne(message.body, env);
          }),
        ),
      );
    },
  },
);

// producer side:
await env.MY_QUEUE.send({
  ...payload,
  auth: { userId, orgId },
  action: "rank-refresh",
});

Scheduled and email

Cron ticks default to action: "scheduled" and email triggers to action: "email". Wrap with trackAction to give them a real name.

ts
scheduled: async (controller, env, ctx) => {
  await trackAction("nightly-rebuild", async () => {
    await rebuildEverything(env);
  });
},

04 // Tagging actions

Tagging actions

Tagging lets the dashboard tell you what each kind of run costs, for exampleai-search · $0.014 avg/run. That is the number you base credit prices on. Three ways, by precedence.

Per queue message

ts
env.MY_QUEUE.send({ ...payload, action: "ai-search" });

Per HTTP block

ts
await trackAction("ai-search", async () => {
  await openai.chat.completions.create(...);
});

Worker shape default

Untagged work falls back to queue, scheduled, or email based on the entry point.

05 // Manual events

Manual events

Hit a provider the lib does not recognize, or already know the cost? Call trackUsage from inside any tracked scope. It inherits the scope's app, user, org, and action. Outside a scope it is a silent no op, so it is safe to leave in.

ts
import { trackUsage } from "@viewengine/track";

const res = await fetch("https://api.some-provider.com/run", { ... });
const body = await res.json();

trackUsage({
  provider: "some-provider",
  costUsd: body.cost,
  model: body.model,
  promptTokens: body.usage?.input,
  completionTokens: body.usage?.output,
  statusCode: res.status,
});

Credits

Billing your users in credits through Autumn or another credit system? Report every deduction with trackCredits and the dashboard breaks credit usage down by app, action, user, org, provider, and model, right next to cost. Call it after your billing call succeeds. Like trackUsage, it inherits the scope's app, user, org, and action, and it is a silent no op outside a scope.

ts
import { trackCredits } from "@viewengine/track";

const result = await autumn.track({
  customerId: user.id,
  featureId: "ai_search",
  value: 3,
});

trackCredits({
  credits: 3,
  action: "ai_search",
});

To attach credits to the same row as the provider call that caused them, pass creditsCharged (and optionally creditPriceUsd) to trackUsage instead. That keeps the deduction on the usage event and powers the profitability view.

Charging the same credit price across all your apps? Set a default once in Settings → Credits and skip creditPriceUsd everywhere. Credit events that arrive without a price pick up the default at ingest. Pass creditPriceUsd only in an app with its own unique pricing, since an explicit value always wins over the default.

06 // Configuration

Configuration

trackHandler(config, handler) accepts:

FieldDefaultNotes
apprequiredStable slug shown on the dashboard, like ve-rank.
apiKeyenv.VE_TRACK_KEYOverride if your secret is named differently.
baseUrltrack.viewengine.aiPoint at staging or a self hosted instance.
resolveUser"clerk"Reads the Clerk session. Pass a custom resolver, or "none" to disable.
maxExtractBytesunlimitedSkip usage extraction for responses whose Content Length exceeds this many bytes. The event still records latency and status.

Resolution is lazy. The resolver runs at most once per request, and only when the request actually records a tracked event: a matched provider fetch, trackUsage, or trackCredits. Requests that never touch a provider pay zero resolution cost. If your app already verifies auth in middleware, pass a custom resolveUser that reads your existing auth context instead of verifying the token a second time.

Custom resolver for non Clerk auth:

ts
trackHandler<Env>(
  {
    app: "my-app",
    resolveUser: async (req, env) => ({
      userId: parseSession(req)?.userId ?? null,
      orgId: parseSession(req)?.tenantId ?? null,
    }),
  },
  { fetch: ... },
);

07 // Providers

Providers

These domains are auto detected and priced for you, no config. Token based LLMs are priced server side from a live catalog; the rest use the cost the provider reports.

OpenAI

api.openai.com

Anthropic

api.anthropic.com

Gemini

generativelanguage.googleapis.com

OpenRouter

openrouter.ai/api

Perplexity

api.perplexity.ai

Cloro

api.cloro.dev

Fal

fal.run

Zyte

api.zyte.com

DataForSEO

api.dataforseo.com

Apify

api.apify.com

Firecrawl

api.firecrawl.dev

BrightData

brightdata.com

Add one with a single entry in src/providers.ts: match the URL, optionally enhance the request, and extract the cost and tokens from the response.

08 // Users and orgs

Users and orgs

With the default resolveUser: "clerk", ve-track attributes each event to the signed in Clerk user and org. If any step fails the request still runs, the event just is not user attributed. Which credentials it reads depends on the env vars you expose.

Env vars presentBehavior
CLERK_SECRET_KEY onlyVerifies the Authorization: Bearer header. Cookie authenticated page requests are not attributed.
+ CLERK_PUBLISHABLE_KEYFull session resolution via Clerk's authenticateRequest, covering both bearer tokens and session cookies. Use this for SSR apps where page loads authenticate via cookies.
+ CLERK_JWT_KEYVerification becomes a local signature check against the PEM public key, no network round trip to Clerk even on cold isolates. Recommended for latency sensitive Workers.

For queue messages, the producer stamps body.auth = { userId, orgId } on the message and trackMessage picks it up. The dashboard resolves IDs to names server side, so you see real people and orgs instead of raw IDs.

Ready to wire it up?

Issue a key and drop the wrapper into your worker.

Get a key