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.
npm install @weckr/sdk openaiimport 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
});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.
OpenAI
→GPT-4o, GPT-4o-mini, GPT-4, GPT-3.5, o1-preview, o1-mini.
Anthropic
→Claude Opus 4, Sonnet 4, Haiku 4.5, plus the 3.5 family.
Google Gemini
→Gemini 2.5 Pro and Flash. Works with @google/genai and the legacy SDK.
LangChain
→Wrap the underlying OpenAI client at the boundary. Python guide.
CrewAI
→Per-user attribution and token-velocity alerts for runaway agent loops.
LlamaIndex
Coming soonRAG pipelines and query engines. Native callback handler on the roadmap.
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.
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
npm install @weckr/sdk3. Initialise once at boot
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),
});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
const result = await wk.chat(openai, {
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
userId: user.id,
feature: 'ai-summary',
plan: user.plan,
});Anthropic
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
// 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 passnull) 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 yourplansconfig. Required for caps and margin. Passing a value not inplansthrowsWeckrConfigErrorat 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.
// 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
WeckrCapErrorbefore 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
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:
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.
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.
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.
# 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 verifyToggle 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.
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_SECRETBearer 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
// 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).
Error handling
The SDK throws two kinds of errors. Tell them apart with the type guards.
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. Yourwk_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 aplantowk.chat()that isn't in the constructor'splansmap. 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.
// 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.
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/projectsand the response includes the freshwk_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-keyand 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]/featuresand 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/checkbefore 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), thenupdateUsers, signs you out, and emails a “Your password was changed” notification through Resend. After success you're redirected to/auth/loginto log in with the new password. Rate-limited to 10 attempts/hour/IP.
SDK reference
WeckrConfig
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)
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?)
// Await all in-flight log POSTs. Default timeout: 5_000 ms.
await wk.flush();
await wk.flush(10_000); // wait up to 10sLong-running servers don't need this; short-lived processes (Lambda, cron, CLI) do. See Short-lived processes.
WeckrCapError
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
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
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
{
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.
// 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.
// 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.
// 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.
// 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,
});(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.
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}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.
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 breakdownGET /api/v1/users/[projectId]— per-user margin breakdownGET /api/v1/users/[projectId]/[userId]/features— expand-row dataGET /api/v1/projects/[projectId]/caps— current spending capsPATCH /api/v1/projects/[projectId]/caps— update spending capsGET /api/v1/recommendations/pricing/[projectId]— per-plan pricing intelGET /api/v1/recommendations/models/[projectId]— model swap recommendationsGET / PATCH /api/v1/projects/[projectId]/alert-config— velocity / margin / Slack / email / weekly-digest configGET /api/v1/projects/[projectId]/alerts?since=24h|7d|30d— historical alert logPOST /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 newwk_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 theweckr_weekly_digestRPC, 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.
| Model | Provider | Input / 1M | Output / 1M | Cheaper alternative |
|---|---|---|---|---|
| gpt-4o | openai | $2.50 | $10.00 | gpt-4o-mini |
| gpt-4o-mini | openai | $0.15 | $0.60 | — |
| gpt-4-turbo | openai | $10.00 | $30.00 | gpt-4o-mini |
| gpt-4 | openai | $30.00 | $60.00 | gpt-4o-mini |
| gpt-3.5-turbo | openai | $0.50 | $1.50 | — |
| o1-preview | openai | $15.00 | $60.00 | — |
| o1-mini | openai | $3.00 | $12.00 | — |
| claude-opus-4 | anthropic | $15.00 | $75.00 | claude-sonnet-4 |
| claude-sonnet-4 | anthropic | $3.00 | $15.00 | claude-haiku-4-5 |
| claude-haiku-4-5 | anthropic | $0.80 | $4.00 | — |
| claude-3-5-sonnet | anthropic | $3.00 | $15.00 | — |
| claude-3-5-haiku | anthropic | $0.80 | $4.00 | — |
| claude-3-opus | anthropic | $15.00 | $75.00 | — |
| gemini-2.5-pro | gemini | $1.25 | $10.00 | gemini-2.5-flash |
| gemini-2.5-flash | gemini | $0.15 | $0.60 | — |
| gemini-1.5-pro | gemini | $1.25 | $5.00 | gemini-2.5-flash |
| gemini-1.5-flash | gemini | $0.075 | $0.30 | — |
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 togpt-4ois roughly a 600-token prompt toclaude-sonnet-4. - When you slice the dashboard by model or provider, token totals compare cleanly within that slice.
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:
// 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:
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 });FAQ
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.
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.
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).
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.
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.
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.
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.