TypeScript SDK
Install from npm (Node 18+ or the browser; dual ESM + CJS; zero runtime dependencies — the
transport is fetch):
npm install @llmjury/sdk
Authentication
The client reads your org's publishable key (llmj_pk_…) from the LLMJURY_API_KEY
environment variable in Node, so new Client() just works once that's set. In the browser (or
anywhere env vars don't reach), pass it explicitly — new Client({ apiKey: 'llmj_pk_…' }). The
publishable key is safe in client-side code: write-only and rate-limited. The key is sent as
X-API-Key; the SDK defaults to the production API (LLMJURY_BASE_URL/baseUrl override for a
local stack). See where keys come from.
Quickstart
import { Client } from '@llmjury/sdk';
// ---- setup, once at startup -------------------------------------------------
// Reads LLMJURY_API_KEY from the environment; defaults to https://api.llmjury.com.
const client = new Client({ experiments: ['checkout-prompt'] }); // prefetch by experiment NAME
const llm = client.wrap(anthropicClient, 'checkout-prompt'); // every model call is now traced
// ---- per request ------------------------------------------------------------
await client.withUser(userId, async () => {
// Variant prompt from client memory; your in-code default survives an outage.
const p = client.getPrompt('checkout-prompt', userId, 'You are a helpful assistant.');
// Call your provider client DIRECTLY — latency/tokens/errors are intercepted.
const response = await llm.messages.create({ model: 'claude-haiku-4-5', max_tokens: 1024,
system: p.prompt, messages: [{ role: 'user', content: userInput }] });
// The ONLY explicit metric: your business outcome.
client.track('business_event', { experiment_id: 'checkout-prompt', user_id: userId,
variant: p.variant, business_metric: 'conversion', value: 1 });
return response;
});What the client does
getPrompt(experiment, user, defaultPrompt)— the recommended entrypoint: resolves the user straight to their variant's prompt text. Your in-code default is returned whenever the config isn't cached, assignment fails, or the variant has no prompt — the app always has a working prompt, even during a full LLMJury outage. Returns{ variant, prompt, fallback }.getVariables(experiment, user, defaults)— same idea for custom variables (model, temperature, …): the variant's configured values merged over your in-code defaults, from client memory — never an API call on the request path.wrap(providerClient, experiment)+withUser(user, fn)— setup-once interception: wrap your OpenAI/Anthropic-style client at startup, bind the user per request withawait client.withUser(userId, async () => { … }), then call the provider directly. Every model call records the exposure plus amodel_callevent with measured latency, tokens, model, and errors — no call-site code. (interceptModelCallis the explicit form.)assign(experiment, user)— the low-level call: deterministic local bucketing (the exact same MurmurHash3 result as Python and Java); returns the variant key ornulluntil the config loads — treatnullas "use control".track(event, payload)— enqueue-only with a timer-driven flush, bounded retries, then an offline spill (FileOfflineStorein Node,IndexedDbOfflineStorein the browser; 24h replay with original timestamps) or a logged drop. Never throws into your app. Withwrap(), the only event you track by hand is yourbusiness_event.- Experiments are addressed by unique name or id — names resolve through the polled config and bucketing always runs on the canonical id, so both address forms assign identically.
- Config is polled from
GET /v1/config(ETag/304, 60s) with an immediate refresh on a newer ingest-ackconfig_version.await client.ready()resolves once prefetched configs are loaded.
Full details in the SDK README.