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,
});

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.

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 reads the Authorization: Bearer header, verifies it with your CLERK_SECRET_KEY, and attributes the event to that user and org. If any step fails the request still runs, the event just is not user attributed.

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