weckrDocs

Documentation

Weckr is AI cost & margin intelligence for SaaS founders. Wrap any LLM client, get per-user and per-feature cost data, set spending caps that the SDK enforces before the LLM call, and receive recommendations on pricing & cheaper models.

This page covers the TypeScript / JavaScript SDK @weckr/sdk@0.1.5. Python users see /docs/python. What changed in each release lives in the changelog.

Quick start

Drop the SDK in front of your existing LLM client. The call returns unchanged; Weckr logs cost + margin asynchronously and enforces caps before each call.

bash
npm install @weckr/sdk openai
ts
import OpenAI from 'openai';
import { Weckr } from '@weckr/sdk';

const openai = new OpenAI();
const wk = new Weckr({
  apiKey: 'wk_...',                  // from app.useweckr.com signup
  plans: { free: 0, pro: 29 },       // your plan prices in USD
});

const result = await wk.chat(openai, {
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'Summarize this doc.' }],
  userId: user.id,                   // your app's user id
  feature: 'doc-summary',            // anything you'd group cost by
  plan: user.plan,                   // matches one of `plans` above
});
That's the whole integration. Your existing call works exactly as before; the dashboard at app.useweckr.com fills with data on the next refresh.

Integrations

Weckr sits in front of the LLM client you already use. Pick the provider you call today, or one of the framework guides if your agent code wraps it in another layer. Browse them all at /docs/integrations.

Weckr vs Helicone

Helicone is the go-to LLM observabilitytool — it sits as a proxy in front of your provider, captures every request and response, and gives you traces, prompt logs, caching, and prompt versioning. If you want to see the body of every prompt, replay traces, or A/B test prompts, use Helicone.

Weckr is a different shape. It's a margin tool for SaaS founders. We don't proxy your calls or capture prompt bodies. We track cost & revenue per (userId, plan) and enforce a monthly spend cap in the SDK, before the LLM call ever happens. The dashboard answers questions Helicone doesn't: which of my usersare unprofitable, what should I charge for the pro plan, and where am I overspending on a flagship model for a feature that doesn't need it.

Practical differences if you're choosing:

  • Proxy vs SDK.Helicone changes your base URL. Weckr wraps your existing client — no DNS, no routing change, your API key stays direct with OpenAI/Anthropic/Google.
  • Per-prompt vs per-user.Helicone's primary unit is a request. Weckr's primary unit is a (user, plan) pair across the month.
  • Enforcement.Weckr will block or downgrade a call when a user hits their cap. Helicone observes, it doesn't enforce.
  • Scope.Weckr is small — an SDK + a margin dashboard. No prompt vault, no eval suite, no caching layer. If you need those, use both.
They're complementary: most teams running both keep Helicone for prompt traces and Weckr for the “is this user costing me money” conversation.

Install & configure

1. Sign up & create a project

Sign up at app.useweckr.com/auth/signup. Right after, you'll create a project and we'll show you its wk_ key once. Copy it — the dashboard masks it from then on.

2. Install the SDK

bash
npm install @weckr/sdk

3. Initialise once at boot

ts
import { Weckr } from '@weckr/sdk';

export const wk = new Weckr({
  apiKey: process.env.WECKR_API_KEY!,
  plans: {
    free:     0,
    starter:  9,
    pro:      29,
    business: 99,
  },
  // optional:
  // onError: (err) => console.error('Weckr:', err),
});
The plansmap is how Weckr knows what each user pays you. It's what powers the margin column on the dashboard and the recommendation engine.

Logging LLM calls

Wrap your existing client — OpenAI, Anthropic, or Gemini — via wk.chat(client, options). The SDK detects the provider, makes the call, returns the original result, and after the call resolves, fires a fire-and-forget log to the Weckr ingest endpoint.

OpenAI

ts
const result = await wk.chat(openai, {
  model: 'gpt-4o',
  messages: [{ role: 'user', content: prompt }],
  userId: user.id,
  feature: 'ai-summary',
  plan: user.plan,
});

Anthropic

ts
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();

const result = await wk.chat(anthropic, {
  model: 'claude-sonnet-4',
  max_tokens: 1024,
  messages: [{ role: 'user', content: prompt }],
  userId: user.id,
  feature: 'ai-essay',
  plan: user.plan,
});

Gemini

ts
// New SDK (recommended)
import { GoogleGenAI } from '@google/genai';
const gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });

const result = await wk.chat(gemini, {
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: prompt }],
  userId: user.id,
  feature: 'ai-translate',
  plan: user.plan,
});

The legacy @google/generative-ai SDK still works — we detect either client shape. The new @google/genai client is recommended for new projects.

Fields you should pass

  • userId — the end-user of your app, stable across requests. Use your auth user id. Required for cap checks and per-user margin. Omit (or pass null) for anonymous calls — the row still lands; cap checks are simply skipped.
  • feature — a label like 'ai-summary' or 'chat-reply'. Groups requests on the Features page and powers model recommendations.
  • plan — matches one of the keys in your plans config. Required for caps and margin. Passing a value not in plans throws WeckrConfigError at call time.

Anonymous calls

For calls that don't belong to a logged-in user (marketing tools, anonymous demos, internal scripts), omit userId, feature, and plan. The row lands with nullin those columns and shows up grouped under “None” on the Users page.

ts
// anonymous — no userId, no plan
await wk.chat(openai, {
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: prompt }],
});

Streaming

stream: trueis rejected synchronously today, because OpenAI/Anthropic don't return token usage on streamed responses by default. If you need streaming, call the underlying client directly outside wk.chat()and you'll lose Weckr cost tracking for those calls. We'll add wrapped streaming with full usage capture in a future release.

Spending caps

Set a max monthly AI spend per user per plan. The SDK checks the cap before every LLM call (with a 60-second in-memory cache per userId+plan, so at most one extra request per user per minute).

1. Configure in the dashboard

On /dashboard/settings set a monthly cap (USD) for each plan and pick the action:

  • Block call — the SDK throws WeckrCapError before reaching the LLM. You catch it and show your user an upgrade prompt.
  • Downgrade model— the SDK silently swaps to a cheaper model in the same provider (e.g. gpt-4o gpt-4o-mini). The call still completes and logs against the new model.

2. Handle the block case

ts
import { isWeckrCapError } from '@weckr/sdk';

try {
  const result = await wk.chat(openai, opts);
  return result;
} catch (err) {
  if (isWeckrCapError(err)) {
    return res.status(402).json({
      error: 'AI spend cap reached for this user',
      cap: err.cap,
      spent: err.currentSpend,
      planName: err.planName,
    });
  }
  throw err;
}

Downgrade is transparent

When the SDK downgrades, it emits a one-time console.warn per (userId, model)pair (so production logs aren't flooded) and the LLM result you receive is from the cheaper model. No throw. The log row records the actually-used model, so your dashboard reflects reality.

Override the warn with an onDowngrade callback if you want to emit analytics or your own log line instead:

ts
const wk = new Weckr({
  apiKey: process.env.WECKR_API_KEY!,
  plans: { /* ... */ },
  onDowngrade: ({ userId, from, to }) => {
    analytics.track('ai_downgrade', { userId, from, to });
  },
});

Fail-open / fail-closed

Cap-check failures are split deliberately by status code so a misconfigured key can't silently disable enforcement:

  • 5xx, 429, network error, timeout— fail OPEN (LLM call proceeds). A blip on our side never breaks your customer's app.
  • 401, 403 — fail CLOSED with a WeckrConfigError. A typo'd or revoked api key throws synchronously so you catch the bug at boot, not silently in production. See Error handling.

Concurrent calls share one /check

N parallel wk.chat() calls for the same (userId, plan, model) share a single in-flight /check request. The result is cached for 60 seconds under a key that includes the model, so a downgrade for gpt-4o doesn't bleed onto a subsequent claude-sonnet-4 call.

You can opt out of cap checks entirely with disableCapCheck: true in WeckrConfig.

Real-time alerts

Weckr fires two kinds of alerts automatically after every successful /api/v1/log POST: a velocity check (catches token-bursting runaway agents) and a margincheck (catches users who cost you more than they pay). Both run as fire-and-forget background work using Vercel's after() primitive so they never block the ingest response.

Velocity / agent loop detection

Sums tokens for the same (projectId, userId) over the last few minutes. If cumulative tokens cross your threshold, Weckr writes an alert_log row and dispatches whatever channels you have configured. Designed for runaway CrewAI / LangChain reasoning loops.

Defaults

  • Window: 5 minutes — how far back we sum tokens.
  • Token threshold: 50,000 — if a single user crosses this in the window, the alert fires.
  • Cooldown: 30 minutes — after one alert fires for a (project, user), we suppress duplicates for this long. Prevents an actively-looping agent from spamming Slack.

Margin alerts

For the same (projectId, userId), Weckr computes the user's current-month margin as MAX(planRevenueUsd) minus SUM(costUsd). If that drops below your configured threshold (a negative number like -5meaning “alert when this user is losing me more than $5/month”), the alert fires through the same Slack + email channels as velocity. Uses the same cooldown logic.

The revenue uses MAXnot SUM, on purpose: every event you log carries the user's plan price at the time of the call, so summing them would multiply revenue by request count. MAX returns the user's actual monthly plan price (handles mid-period plan upgrades by taking the highest tier they were on).

No code change required

Both detections run server-side after each ingest. You don't add any code in your app — if you're already calling wk.chat() with a userId and a plan, you already have both.

Tune in the dashboard

All knobs live in /dashboard/settings under Agent loop detection and Margin alerts.

The CrewAI guide shows the velocity pattern in practice. Loop detection is the single biggest reason agent founders adopt Weckr.

Notifications: Slack & email

When loop detection trips (or any future alert type), Weckr can ping Slack and email. Both are configured per-project on /dashboard/settings and stored in the project's alert_config jsonb.

Slack

Paste an incoming webhook URL. We POST a red-attachment payload with the userId, tokens-in-window, cost so far, request count, and a link back to the dashboard. No app install on Slack's side.

bash
# 1. Create a Slack incoming webhook
#    https://api.slack.com/messaging/webhooks
# 2. Paste the URL into /dashboard/settings → Agent loop detection
# 3. Click "Test alert" to verify

Email

Toggle email alerts on, enter the destination address. Emails ship via Resend from the Weckr-verified sender domain — no SMTP setup on your end.

Test alert button

Settings has a Test alert button next to the channel inputs. It POSTs /api/v1/projects/[id]/test-alert which dispatches a fake velocity alert through every channel you've configured. Lets you confirm Slack + email work before a real anomaly trips them.

The cooldown applies to real alerts only — the test button bypasses it so you can re-test without waiting.

Weekly digest

A Monday 09:00 UTC Vercel cron iterates every project with Weekly digest enabled in Settings and emails the configured address: last-week total cost / revenue / margin, request count, top-3 users and top-3 features by spend, and the week-over-week cost delta. Sent through Resend the same way alert emails are.

  • Empty weeks still send (a quiet inbox digest is a useful signal — a missing one isn't).
  • Cron auth uses a single shared CRON_SECRET Bearer header; you don't need to configure anything project-side.
  • Email addresses go through a strict single-address validator before Resend (no CRLF, no comma fan-out).

Query from Claude (MCP server)

@weckr/mcp is a Model Context Protocol server. Plug it into Claude Desktop, Cursor, or any MCP-compatible client and ask your cost data questions in plain language.

Install

json
// Claude Desktop config (claude_desktop_config.json)
{
  "mcpServers": {
    "weckr": {
      "command": "npx",
      "args": ["-y", "@weckr/mcp"],
      "env": {
        "WECKR_API_KEY": "wk_..."
      }
    }
  }
}

Cursor uses the same JSON in .cursor/mcp.json. Restart your client and the Weckr tools appear.

Tools exposed

  • get_overview— total cost, revenue, margin, request count, unprofitable user count for the month.
  • get_users— per-user margin breakdown, filterable.
  • get_feature_breakdown— per-feature cost share.
  • get_model_recommendations— same-provider cheaper-model swaps with $ saving estimates.
  • get_pricing_recommendations— per-plan margin health + recommended price.
  • get_spending_cap_url— returns the dashboard URL where caps are edited (does NOT mutate).
Sample prompts that work well: “Which users are unprofitable this month?” · “Where can I cut AI cost?” · “Is my Pro pricing sustainable?” Source code lives at github.com/Ghiles3232/weckr-sdks/tree/main/mcp.

Error handling

The SDK throws two kinds of errors. Tell them apart with the type guards.

ts
import {
  isWeckrCapError,
  isWeckrConfigError,
} from '@weckr/sdk';

try {
  const result = await wk.chat(openai, opts);
} catch (err) {
  if (isWeckrCapError(err)) {
    // User hit their monthly cap. Show an upgrade prompt.
    // err.userId / err.planName / err.cap / err.currentSpend are populated.
    return showUpgradePrompt(err);
  }
  if (isWeckrConfigError(err)) {
    // CRITICAL — Weckr is misconfigured. Send to your backend alerts.
    // err.code is one of:
    //   'invalid_api_key' | 'forbidden' | 'unknown_plan'
    return alertBackend(err.code, err.message);
  }
  // Any other error is from the LLM provider itself — your problem.
  throw err;
}
  • WeckrCapError — cap reached. User-visible: show your upgrade UI.
  • WeckrConfigError developer-only. Three codes:
    • invalid_api_key: 401 from /check. Your wk_ key is typo'd, revoked, or wasn't copied correctly.
    • forbidden: 403 from /check. Less common; usually means the project was deleted.
    • unknown_plan: you passed a plan to wk.chat() that isn't in the constructor's plans map. Fail-fast so a typo doesn't silently poison your dashboard with phantom $0-revenue users.
onError receives async errors (log POST failures, cap-check 5xx). It does NOT receive WeckrCapError or WeckrConfigError — those are synchronous throws.

Short-lived processes (Lambda, cron, CLI)

wk.chat()returns as soon as the LLM call resolves; the log POST runs on a microtask after that. In a long-running web server this is perfect — it adds zero hot-path latency. In a short-lived process (Lambda handler, cron job, CLI), the host can terminate before the POST hits the network and you lose the last log row.

Call await wk.flush() before exit. It awaits every in-flight POST (default 5s timeout) and returns.

ts
// Lambda handler
export const handler = async (event) => {
  const result = await wk.chat(openai, {
    model: 'gpt-4o-mini',
    messages: [...],
    userId: event.userId,
    plan: 'pro',
  });

  await wk.flush();    // wait for the log POST before Lambda freezes the runtime
  return result;
};

Long-running servers (Express, Next.js routes, etc.) don't need this — the POST completes naturally before the process ever exits.

Pricing intelligence

Once your project has enough data this month (5+ distinct users and 30+ requests), the Overview page surfaces a per-plan recommendation:

  • User count & avg cost per user— how many distinct users on this plan and what each one costs you on average.
  • Unprofitable %— share of users on this plan whose AI cost exceeds the plan's revenue.
  • Recommended price avg_cost * 1.4, targeting a 40% margin.
  • Potential monthly gain (recommended - current) * user_count.

Card border colour reflects risk: green for healthy plans, amber 20–50% unprofitable, red>50%.

Model recommendations

The Recommendations page suggests cheaper models for features that don't need the heavy ones. Heuristic: when a feature's average output is under 150 tokens this month and a cheaper model exists in the same provider, we propose the swap.

Each card shows the projected monthly cost on the cheaper model based on real token volume, the estimated saving, and a one-click copy model name button so you can paste it straight into your code.

Recommendations appear only when the saving is at least $1/month— below that, we don't want to waste your attention.

The dashboard

All pages live under app.useweckr.com/dashboard. Every page reads project-scoped data and respects the currently active project (picker in the top nav). Pages that depend on an active project gracefully fall back to a “Create your first project” card if you have none. You can try every page without signing up at /demo.

Top navigation bar

  • Project picker— switches the active project. Every dashboard page re-renders in place with the new project's data. No reload, no URL change.
  • + New project— opens the new-project form. Submitting POSTs /api/v1/projects and the response includes the fresh wk_ API key.

Sidebar

  • Nav links— Overview, Users, Features, Loop activity, Alerts, Recommendations, Pricing intel, Settings. The active page gets a filled background.
  • Recommendations amber dot— appears automatically when there's at least one model-recommendation worth looking at.
  • API key card— shows the active project's key (masked). Click to copy. Rotate link opens a confirmation modal; on confirm it POSTs /api/v1/projects/[id]/rotate-key and transitions the modal to a one-shot reveal panel showing the new plaintext key (the server only returns it once). Cancel is autofocused; the old key stops working immediately after rotation.
  • Account— link to the account page (see below).
  • Docs— opens this page in a new tab.
  • Logout— signs out via Supabase and redirects to /auth/login.

Overview — /dashboard/overview

The default landing page. Reads from /api/v1/stats/[projectId].

  • Losing-money banner— only shown when at least one user has negative margin this month. Counts and total loss link directly to the Users page.
  • 4 stat cards— total AI cost, total revenue, net margin, count of unprofitable users. Unprofitable count gets a red “Action needed” badge when > 0.
  • Loop activity card— live token-velocity summary card (count of alerts in last 24h, recent burner).
  • Cost vs revenue chart— 30-day line chart of daily totals.
  • Pricing intel card— teaser of the per-plan pricing recommendations. Full view on the dedicated page.
  • First-event empty state— when a freshly created project has zero events, the page replaces the all-zero stat cards with an onboarding card that has copy-pasteable TypeScript / Python / MCP code snippets pre-filled with your API key.

Users — /dashboard/users

One row per userIdyou've logged this month. Reads /api/v1/users/[projectId].

  • Default sort: margin ascending (worst first). Click any column header to re-sort.
  • Status pill: unprofitable (red), watch (amber) when margin is thin, healthy (green) otherwise.
  • Click any row to expand inline: fetches /api/v1/users/[projectId]/[userId]/features and shows per-feature cost breakdown for that single user.

Features — /dashboard/features

One row per featurestring you've sent. Columns: feature, total cost, request count, avg cost per request, % of total spend. Sorted by total cost descending. Reads /api/v1/stats/[projectId] (same RPC as Overview, different slice).

Loop activity — /dashboard/loops

Live operations view for velocity anomalies. Three sections:

  • Thresholds— read-only summary of the current velocity config (status, window, token threshold, cooldown). Edit link goes to Settings.
  • Live token velocity— chronological list of velocity alerts in the last 24h. Empty state when calm.
  • Top burners (last hour)— users sorted by cumulative tokens burned in the last 60 minutes, derived client-side from the same alert feed (no extra request). Capped to 10.

Alerts — /dashboard/alerts

The historical audit table of every alert dispatched (velocity or margin), with a time-window chip row (Today / 7 days / 30 days). Reads /api/v1/projects/[id]/alerts?since=.... Click any row to expand and see the raw metadata JSON that was stored on the alert (token total, threshold, cost, request count, etc).

Recommendations — /dashboard/recommendations

Per-feature model-swap suggestions. Each card shows the current model, the recommended cheaper model, estimated monthly savings, and a Copy button to grab the recommended model string. Big wins ($50+ projected savings) get an accent border. Reads /api/v1/recommendations/models/[projectId].

Pricing intel — /dashboard/pricing

Per-plan pricing recommendations driven by your actual cost-per-user. Each card shows current price, recommended price, % of users on that plan who are unprofitable, total monthly loss from the plan, and the recommended action (raise the price by X, cap free users at Y, etc). Border tone reflects severity (red / amber / green). Reads /api/v1/recommendations/pricing/[projectId].

Settings — /dashboard/settings

All project configuration. Four cards plus a danger zone. Reads /writes /api/v1/projects/[id]/caps and /api/v1/projects/[id]/alert-config.

1. Spending caps

  • Per-plan monthly USD limits. The SDK reads these via /api/v1/check before every LLM call.
  • For each plan, set a cap and choose the action: Block call (throws WeckrCapError) or Downgrade model (silently swaps to the cheaper tier in the same provider, user notices nothing).
  • Save capsbutton at the bottom of the card shows “Saved ✓” on success.

2. Agent loop detection

  • Enabled toggle— when off, the backend skips the velocity check on every /log.
  • Window (minutes)— how far back to sum tokens. 5 minutes is the default.
  • Token threshold— tokens per window that trip an alert. 50,000 is the default.
  • Cooldown (minutes)— minimum gap between two alerts for the same (project, user). 5–120 valid; default 30.
  • Slack webhook URL— paste a Slack incoming-webhook URL. We POST a red-attachment payload.
  • Alert email— address that receives the alert email through Resend. Strict single-address validation.
  • Test alertbutton — dispatches a fake velocity alert through every configured channel. Bypasses cooldown.

3. Margin alerts

  • Margin alert threshold (USD per user)— a non-positive number (e.g. -5). When a user's current-month margin (plan revenue minus AI cost) drops below this value, the alert fires through the same Slack + email channels as velocity.
  • Uses the same cooldown as velocity. Same channels respected (Slack toggle, email toggle).

4. Weekly digest

  • Enable toggle and a separate Digest email field (can differ from your alert email).
  • When enabled, a Monday 09:00 UTC Vercel cron emails this address: last week total cost / revenue / margin, request count, top-3 users and top-3 features by spend, and week-over-week cost delta.

Danger zone

At the bottom of Settings, in a red-bordered card. Delete this projectopens a confirmation modal that requires you to type the project's name to enable the delete button. On confirm it sends DELETE /api/v1/projects/[id] (which verifies ownership via Supabase RLS), removes the project plus every dependent row via ON DELETE CASCADE (requests, alert_log, alert_config, caps), refreshes the project context, and redirects you to /dashboard/overview. Irreversible.

Account — /dashboard/account

Your account settings. Two cards:

  • Email— read-only display of the address you sign in with. Pulled from the active Supabase session.
  • Change password— current password, new password, retype new password. POSTs /api/v1/auth/change-passwordwhich verifies the current password using a separate cookieless Supabase client (so a wrong-current or weak-new failure doesn't touch your live session), then updateUsers, signs you out, and emails a “Your password was changed” notification through Resend. After success you're redirected to /auth/login to log in with the new password. Rate-limited to 10 attempts/hour/IP.

SDK reference

WeckrConfig

ts
interface WeckrConfig {
  apiKey: string;                            // required, starts with 'wk_'
  plans?: Record<string, number>;            // plan name -> monthly price USD
  endpoint?: string;                         // default: 'https://app.useweckr.com/api/v1/log'
  checkEndpoint?: string;                    // default: derived from endpoint
  disableCapCheck?: boolean;                 // default: false
  fetch?: typeof fetch;                      // inject a custom fetch impl
  onError?: (err: unknown) => void;          // observe async internal errors
  onDowngrade?: (info: {                     // emitted when a cap-downgrade swaps the model
    userId: string;
    from: string;
    to: string;
  }) => void;
}

wk.chat(client, options)

ts
interface ChatOptions {
  model: string;                             // e.g. 'gpt-4o' or 'gpt-4o-2024-08-06'
  messages: { role: string; content: unknown }[];
  userId?: string;                           // your app's user id (omit for anonymous)
  feature?: string;                          // e.g. 'ai-summary'
  plan?: string;                             // MUST match a key in `plans` (or omit)
  // any other fields are forwarded to the underlying client
}

const result: TResult = await wk.chat<TClient, TResult>(client, options);

Passing stream: trueis rejected synchronously because token usage isn't available on streamed responses by default. Disable streaming for now; we'll add wrapped streaming with stream_options: { include_usage: true } in a future release.

wk.flush(timeoutMs?)

ts
// Await all in-flight log POSTs. Default timeout: 5_000 ms.
await wk.flush();
await wk.flush(10_000);   // wait up to 10s

Long-running servers don't need this; short-lived processes (Lambda, cron, CLI) do. See Short-lived processes.

WeckrCapError

ts
class WeckrCapError extends Error {
  name: 'WeckrCapError';
  userId: string;
  planName: string;
  currentSpend?: number;
  cap?: number;
}

import { isWeckrCapError } from '@weckr/sdk';
if (isWeckrCapError(err)) { /* show upgrade prompt */ }

WeckrConfigError

ts
class WeckrConfigError extends Error {
  name: 'WeckrConfigError';
  code: 'invalid_api_key' | 'forbidden' | 'unknown_plan';
}

import { isWeckrConfigError } from '@weckr/sdk';
if (isWeckrConfigError(err)) { /* alert backend, this is a misconfig */ }

Pure-function exports

ts
import { PRICING, resolvePricing, calculateCost } from '@weckr/sdk';

PRICING['gpt-4o'];                      // { provider, inputPerMillion, outputPerMillion }
resolvePricing('gpt-4o-2024-08-06');    // resolves to gpt-4o family pricing
calculateCost('gpt-4o-mini', 12, 2);    // { costUsd: 0.000003, provider: 'openai' }

Dated variants (gpt-4o-2024-08-06, claude-3-5-sonnet-latest, etc.) resolve via longest-prefix match against PRICING. Unknown models return null / costUsd: 0 rather than throwing.

What gets logged

ts
{
  userId,             // string | null
  feature,            // string | null
  model,              // the actually-used model (downgraded if applicable)
  provider,           // 'openai' | 'anthropic' | 'gemini'
  inputTokens,
  outputTokens,
  costUsd,            // server-recalculated; client value is ignored
  latencyMs,
  planName,           // string | null
  planRevenueUsd,     // number | null  — looked up from your plans map
  marginUsd,          // sent for backward-compat; server derives at read time
  timestamp,          // ISO 8601 UTC
}

Logging is fire-and-forget — failures never bubble up to your code (they go to onError if you provided one). Cost is recomputed server-side from (model, tokens) — a wk_ key holder cannot forge a fake cost value. Margin is derived in the read RPCs as SUM(plan_revenue_usd) - SUM(cost_usd) at full precision, so users whose calls cost sub-cent are still detected as unprofitable on a $0-revenue plan.

Common patterns

Four real-world shapes for dropping Weckr into a production codebase. Each example assumes you already have an @weckr/sdk install and a wk_ key.

Next.js Route Handler (App Router)

Most Next.js teams put Weckr in a singleton import so it's shared across hot reloads and serverless invocations.

ts
// lib/weckr.ts — module-level singleton
import { Weckr } from '@weckr/sdk';

export const wk = new Weckr({
  apiKey: process.env.WECKR_API_KEY!,
  plans: { free: 0, pro: 29, business: 99 },
});

// app/api/summarize/route.ts
import OpenAI from 'openai';
import { wk } from '@/lib/weckr';
import { isWeckrCapError } from '@weckr/sdk';
import { auth } from '@/lib/auth';
import { NextResponse } from 'next/server';

const openai = new OpenAI();

export async function POST(req: Request) {
  const session = await auth();
  if (!session?.user) return new Response('Unauthorized', { status: 401 });
  const { text } = await req.json();

  try {
    const result = await wk.chat(openai, {
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: `Summarize: ${text}` }],
      userId: session.user.id,
      feature: 'doc-summary',
      plan: session.user.plan,
    });
    return NextResponse.json({ summary: result.choices[0].message.content });
  } catch (err) {
    if (isWeckrCapError(err)) {
      return NextResponse.json(
        { error: 'Upgrade required', upgradeUrl: '/billing' },
        { status: 402 },
      );
    }
    throw err;
  }
}

Express middleware

Attach the Weckr instance to the request so every route has a user-scoped client. Avoids repeating userId + plan at every call site.

ts
// middleware/weckr.ts
import type { Request, Response, NextFunction } from 'express';
import { Weckr } from '@weckr/sdk';

const wk = new Weckr({
  apiKey: process.env.WECKR_API_KEY!,
  plans: { free: 0, pro: 29 },
});

// Attaches a partial-applied wk.chat that auto-fills userId + plan.
export function weckrMiddleware(req: Request, _res: Response, next: NextFunction) {
  const user = req.user;  // populated by your auth middleware upstream
  req.weckrChat = (client, options) =>
    wk.chat(client, { ...options, userId: user.id, plan: user.plan });
  next();
}

// app.ts
import express from 'express';
import OpenAI from 'openai';
import { weckrMiddleware } from './middleware/weckr';

const app = express();
const openai = new OpenAI();
app.use(authMiddleware);     // sets req.user
app.use(weckrMiddleware);

app.post('/api/chat', async (req, res) => {
  const result = await req.weckrChat(openai, {
    model: 'gpt-4o-mini',
    messages: req.body.messages,
    feature: 'chat',
  });
  res.json(result);
});

Background job — cron, Lambda, CLI

The SDK is fire-and-forget by design — log POSTs queue and flush in the background. In long-running servers that's perfect. In short-lived processes (Lambda handlers, cron jobs, scripts) you must call wk.flush() before exit, otherwise the process exits before the log POST goes out and you lose events.

ts
// AWS Lambda handler (or any short-lived process)
import OpenAI from 'openai';
import { Weckr } from '@weckr/sdk';

const openai = new OpenAI();
const wk = new Weckr({
  apiKey: process.env.WECKR_API_KEY!,
  plans: { free: 0, pro: 29 },
});

export async function handler(event: { userId: string; prompt: string }) {
  try {
    const result = await wk.chat(openai, {
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: event.prompt }],
      userId: event.userId,
      feature: 'lambda-summary',
      plan: 'pro',
    });
    return { summary: result.choices[0].message.content };
  } finally {
    // CRITICAL: wait for any pending log POSTs to land before Lambda freezes
    // the execution context. 5s default timeout; longer for slow networks.
    await wk.flush(5_000);
  }
}
wk.flush() resolves once every queued log POST has either completed or hit the timeout. Always call it in a finally block so it runs even on throws.

Multi-tenant SaaS — per-tenant plans + projects

Two valid shapes when your app sells to other companies:

  • One Weckr project per customer — lets each tenant's users be cleanly isolated in the dashboard. Keep a per-tenant wk_ key in your secrets store and pick the right one at request time.
  • One Weckr project + tenant-prefixed userId — simpler if you don't need per-tenant dashboards. Prefix every userId with the tenant id (e.g. tenant_acme:user_42) so spend aggregates cleanly.
ts
// Pattern A: per-tenant project + key. Cache the Weckr instances.
import { Weckr } from '@weckr/sdk';

const weckrCache = new Map<string, Weckr>();

function getWeckr(tenant: { id: string; weckrApiKey: string; plans: Record<string, number> }) {
  let wk = weckrCache.get(tenant.id);
  if (!wk) {
    wk = new Weckr({ apiKey: tenant.weckrApiKey, plans: tenant.plans });
    weckrCache.set(tenant.id, wk);
  }
  return wk;
}

// in a request handler
const wk = getWeckr(req.tenant);
const result = await wk.chat(openai, {
  model: 'gpt-4o-mini',
  messages: req.body.messages,
  userId: req.user.id,           // the tenant's end-user
  feature: 'chat',
  plan: req.user.plan,           // matches a key in tenant.plans
});

// Pattern B: shared project, tenant-prefixed userId.
const wk = new Weckr({
  apiKey: process.env.WECKR_API_KEY!,
  plans: { free: 0, pro: 29, business: 99 },
});

await wk.chat(openai, {
  model: 'gpt-4o-mini',
  messages: req.body.messages,
  userId: `tenant_${req.tenant.id}:${req.user.id}`,
  feature: `tenant_${req.tenant.id}:chat`,   // optional — for per-tenant feature views
  plan: req.user.plan,
});
For Pattern A, set spending caps per tenant in their own dashboard. For Pattern B, set a single set of caps that apply to every tenant uniformly — the prefix only affects how rows are grouped, not how caps are enforced (caps fire on (userId, planName), prefix included).

HTTP API reference

The SDK is a thin wrapper around a small public API. You can integrate directly from any language. Base URL: https://app.useweckr.com.

POST /api/v1/log

Records a single LLM call. Auth via x-api-key header. userId, feature, planName, planRevenueUsd may all be null or omitted (the row lands as NULL). model, provider, tokens, and latencyMs are required.

bash
curl -X POST https://app.useweckr.com/api/v1/log \
  -H "Content-Type: application/json" \
  -H "x-api-key: wk_..." \
  -d '{
    "userId": "user_123",
    "feature": "summary",
    "model": "gpt-4o-mini",
    "provider": "openai",
    "inputTokens": 120,
    "outputTokens": 80,
    "costUsd": 0.0001,
    "latencyMs": 800,
    "planName": "pro",
    "planRevenueUsd": 29.00
  }'
# -> {"ok": true}
Server-side guarantees: costUsd is recomputed from (model, tokens) — the value you send is discarded. marginUsd is accepted for backward-compat but no longer persisted; the dashboard derives margin as SUM(revenue) - SUM(cost) at read time. If the model has known pricing AND planName is set, posting inputTokens=outputTokens=0 is rejected (400) — a cap-bypass guard.

GET /api/v1/check

Checks whether a given userId on a given planName is over their cap. Auth via x-api-key.

bash
curl "https://app.useweckr.com/api/v1/check?userId=user_123&planName=pro&model=gpt-4o" \
  -H "x-api-key: wk_..."
# -> {
#      "allowed": true,
#      "currentSpend": 4.20,
#      "cap": 20,
#      "remainingBudget": 15.80
#    }
# OR when capped:
# -> {
#      "allowed": false,
#      "action": "downgrade",
#      "alternativeModel": "gpt-4o-mini",
#      "currentSpend": 21.00,
#      "cap": 20,
#      "remainingBudget": 0
#    }

Dashboard endpoints

These power the UI. They require a Authorization: Bearer <jwt> header from a Supabase user session, and only return rows the user owns.

  • GET /api/v1/stats/[projectId]— KPIs + cost chart + feature breakdown
  • GET /api/v1/users/[projectId]— per-user margin breakdown
  • GET /api/v1/users/[projectId]/[userId]/features — expand-row data
  • GET /api/v1/projects/[projectId]/caps— current spending caps
  • PATCH /api/v1/projects/[projectId]/caps— update spending caps
  • GET /api/v1/recommendations/pricing/[projectId] — per-plan pricing intel
  • GET /api/v1/recommendations/models/[projectId] — model swap recommendations
  • GET / PATCH /api/v1/projects/[projectId]/alert-config — velocity / margin / Slack / email / weekly-digest config
  • GET /api/v1/projects/[projectId]/alerts?since=24h|7d|30d — historical alert log
  • POST /api/v1/projects/[projectId]/test-alert — dispatches a fake velocity alert through configured channels (bypasses cooldown)
  • POST /api/v1/projects/[projectId]/rotate-key — mints a new wk_ key; old one stops working immediately. Returns the plaintext key once.
  • DELETE /api/v1/projects/[projectId]— deletes the project and every dependent row via ON DELETE CASCADE. Irreversible.
  • POST /api/v1/projects— creates a new project. Body: { name, plans?: { name, priceUsd }[] }.
  • POST /api/v1/auth/change-password— rotates the signed-in user's password. Body: { currentPassword, newPassword }. Verifies current with a cookieless side client (won't rotate your session on failure), updates, signs out, emails confirmation. Rate-limited 10/hr/IP.

Cron endpoint (Vercel-only)

Internal endpoint invoked by Vercel cron. Not customer-facing — documented here for completeness. Bearer-protected with a single shared CRON_SECRET; the comparison is timing-safe.

  • GET /api/cron/weekly-digest— schedule: 0 9 * * 1 (Mondays 09:00 UTC). Pulls every project opted into weekly digest, computes 7-day stats via the weckr_weekly_digest RPC, emails each via Resend. Returns { processed, sent, errors }.

Auth headers summary

  • SDK callers use x-api-key: wk_.... Endpoints: /log, /check, /me.
  • Dashboard callers use Authorization: Bearer <supabase-jwt>. Everything else.
  • Cron uses Authorization: Bearer $CRON_SECRET.

Supported models

Per-million token prices used for cost calculation and recommendations. Dated variants resolve via longest-prefix match: gpt-4o-2024-08-06 resolves to gpt-4o; claude-3-5-sonnet-latest resolves to claude-3-5-sonnet.

ModelProviderInput / 1MOutput / 1MCheaper alternative
gpt-4oopenai$2.50$10.00gpt-4o-mini
gpt-4o-miniopenai$0.15$0.60
gpt-4-turboopenai$10.00$30.00gpt-4o-mini
gpt-4openai$30.00$60.00gpt-4o-mini
gpt-3.5-turboopenai$0.50$1.50
o1-previewopenai$15.00$60.00
o1-miniopenai$3.00$12.00
claude-opus-4anthropic$15.00$75.00claude-sonnet-4
claude-sonnet-4anthropic$3.00$15.00claude-haiku-4-5
claude-haiku-4-5anthropic$0.80$4.00
claude-3-5-sonnetanthropic$3.00$15.00
claude-3-5-haikuanthropic$0.80$4.00
claude-3-opusanthropic$15.00$75.00
gemini-2.5-progemini$1.25$10.00gemini-2.5-flash
gemini-2.5-flashgemini$0.15$0.60
gemini-1.5-progemini$1.25$5.00gemini-2.5-flash
gemini-1.5-flashgemini$0.075$0.30
Don't see your model? Unknown models still log a row but with costUsd: 0, so caps won't fire on them. Open an issue on GitHub with the model id and we'll ship pricing in a patch.

Token counting across providers

Each provider uses its own tokenizer. The same prompt produces a different token count at OpenAI vs. Anthropic vs. Gemini — they can disagree by 10–25% on the same text. The cost numbers still compare correctly across providers (cost is always price-per-token × tokens, in USD), but raw token counts are not comparable.

  • Do compare across providers: costUsd, marginUsd, requestCount, latency.
  • Don't compare across providers: inputTokens, outputTokens. A 500-token prompt to gpt-4o is roughly a 600-token prompt to claude-sonnet-4.
  • When you slice the dashboard by model or provider, token totals compare cleanly within that slice.
We're working on a normalized “billable units” metric that converts every provider’s tokens to a common unit (essentially: cost ÷ lowest-tier price), so you can rank models on volume regardless of tokenizer. Tracking it in the changelog.

PII and user identifiers

The SDK never sends the text of your prompts or the LLM's responses. But you control what goes in userId, feature, and plan. Common mistakes:

ts
// DON'T — userId looks like PII
await wk.chat(openai, {
  ...,
  userId: 'jane@acme.com',                       // server rejects with 400
  feature: 'ai-summary-jane@acme.com-resume',    // same
});

// DO — opaque identifier you control
await wk.chat(openai, {
  ...,
  userId: 'u_42_abc',                             // your auth user id
  feature: 'ai-summary',                          // static label
});

The ingest endpoint rejects any userId or featurethat contains an email or a credit-card-shaped digit run with a clear 400. We'd rather block these at integration time than discover months later that your dashboard is full of customer PII.

If you need to attribute a row to an email address — e.g. your support tool needs to look up which customer made a call — pass a hash:

ts
import { createHash } from 'node:crypto';

const userIdHash = createHash('sha256')
  .update(user.email.toLowerCase())
  .digest('hex')
  .slice(0, 16);

await wk.chat(openai, { ..., userId: userIdHash, plan: user.plan });
The same restriction applies to the HTTP API. If you're integrating directly without the SDK, sanitise these fields on your side or the POST will 400.

FAQ

Does Weckr add latency to my LLM calls?

The log post is fire-and-forget after your LLM call returns — it doesn't wait. The cap check before the call hits a 60-second in-memory cache, so at most one extra round-trip per (user, plan) per minute. Typical hot path: 0 ms added.

What happens if your API is down?

5xx, 429, or network errors on the cap-check endpoint fail open— the LLM call proceeds, your customer's app keeps running. 401/403 (typo'd or revoked api key) fail closed with a WeckrConfigErrorso a misconfig doesn't silently disable cap enforcement. Log POSTs are always best-effort; failures go to onError if you provided one.

Does the SDK send my prompts or completions to Weckr?

No. The SDK only sends metadata: model, provider, token counts, latency, your userId / feature / planlabels. The prompt text and the model's reply never leave your process — that's a deliberate scope choice (use Helicone if you want full traces).

Can I self-host?

Not yet. The SDKs at github.com/Ghiles3232/weckr-sdks are open source and MIT-licensed — fork the clients freely. The backend (Next.js app, Supabase schema, alert dispatch, cron) is currently a private repo. If self-hosting is a hard requirement, email hello@useweckr.com — we'll open the backend sooner if there's real demand. In the meantime, the SDK's endpoint option accepts any URL, so you can point it at a compatible /api/v1/log + /api/v1/check server you run yourself.

How does cost get calculated?

From the model's public per-token pricing (see the table above) multiplied by the input/output token counts reported by the provider. Margin is planRevenueUsd - costUsd.

What if a user uses multiple plans in one month?

The cap is per (userId, planName). Their totalMTD spend across all plans is compared against the cap for the plan they're currently on. Almost always fine for SaaS; if a user actually switches plans mid-month, the carry-over is generally what you want anyway.

Where's my API key if I forgot to copy it?

Sidebar in the dashboard — click the masked wk_… to copy. We always show the full key to the project owner.

Questions?

Open a discussion on GitHub, browse the roadmap, check what shipped recently in the changelog, or email hello@useweckr.com. We read everything.