Skip to content

TypeScript SDK Reference

Reference for every public symbol exported from @paygentjs/sdk. Anything not on this page (modules with a leading _, internal helpers) is implementation detail and may change between minor versions.

import {
  Paygent,
  paygentContext,
  track,
  getCurrentContext,
  instrument,
  wrap,
  customProvider,
  PaygentLimitExceeded,
  checkGuard,
  createDefaultRegistry,
  ProviderRegistry,
  OPENAI_PROVIDER,
  ANTHROPIC_PROVIDER,
} from "@paygentjs/sdk";

import type {
  PaygentOptions,
  WrapOptions,
  PlanConfig,
  ModelCostRate,
  ModelLimitConfig,
  CurrentUsage,
  BillingPeriod,
  UserState,
  GuardResult,
  GateStatus,
  GateEvent,
  UsageEvent,
  TokenInfo,
  UsageSnapshot,
  MaxTokensAdvice,
} from "@paygentjs/sdk";

// Framework integrations (subpath exports):
import { PaygentCallbackHandler } from "@paygentjs/sdk/integrations/langchain";
// pg.aiMiddleware() — Vercel AI SDK — is a method on the Paygent instance.

Requires Node 18+ (global fetch + AsyncLocalStorage). Ships dual ESM + CJS with types.


Lifecycle

Paygent.init(options?)

static init(options?: PaygentOptions): Promise<Paygent>

Creates and initializes the Paygent singleton, bootstrapping the backend connection and background sync. async — always await it.

Option Default Description
apiKey undefined Backend API key (pg_live_...). Omit for local/offline mode.
baseUrl https://api.paygent.to Backend base URL.
planConfig undefined A Partial<PlanConfig> applied to users until the backend serves a per-user one (or while it's unreachable). In local mode this is the effective plan for every user.
storage "memory" Offline event cache. "sqlite" requires the optional better-sqlite3 peer.
sqlitePath SQLite file path when storage: "sqlite".
flushIntervalMs 5000 Background flush cadence (ms).
autoInstrument false Globally patch installed OpenAI/Anthropic SDKs so every client is metered without instrument(). Opt-in — it mutates shared prototypes; close() restores them.
registry undefined Custom ProviderRegistry (advanced).
const pg = await Paygent.init({ apiKey: process.env.PAYGENT_API_KEY });

pg.flush()

flush(): Promise<void>

Force-drains the event + gate-event queues to the backend. Call before a short-lived process exits so no events are lost.

pg.close()

close(): Promise<void>

Flushes, stops the background timer, and — if autoInstrument was used — restores the patched provider prototypes. Idempotent. This is the teardown call (the analog of the Python SDK's shutdown()).


Context

paygentContext(ctx, fn)

function paygentContext<T>(ctx: PaygentContext, fn: () => T): T

Runs fn with the given context active (via AsyncLocalStorage). Every instrumented LLM call inside — including across await — is attributed to ctx.userId. Returns whatever fn returns.

await paygentContext({ userId: "user_123" }, async () => {
  await openai.chat.completions.create({ /* ... */ });
});

PaygentContext: { userId: string; sessionId?: string; plan?: string; metadata?: Record<string, unknown> }.

track(ctx, fn)

function track<A extends unknown[], R>(
  ctx: PaygentContext | ((...args: A) => PaygentContext | undefined),
  fn: (...args: A) => R,
): (...args: A) => R

Wraps a function so every invocation runs inside a context — the decorator-free analog of Python's @paygent_track. ctx may be a static object or a resolver computed from the arguments. If it resolves to no user, fn runs untracked (fail-open).

getCurrentContext()

function getCurrentContext(): PaygentContext | undefined

The active context, or undefined when none is set.


Instrumentation

pg.instrument(client)

instrument<T extends object>(client: T): T

Returns a Proxy-wrapped copy of a provider client (OpenAI / Anthropic). Calls made inside a paygentContext are guarded (may throw PaygentLimitExceeded) → executed → metered → synced. Calls with no active user pass through untouched. The returned object has the same surface as the original.

const openai = pg.instrument(new OpenAI());

pg.wrap(fn, options?)

wrap<T>(fn: () => Promise<T> | T, options?: WrapOptions): Promise<T>

Explicitly guard + meter a single call without wrapping the whole client. fn performs the actual request.

WrapOptions:

Field Description
userId End-user id. Falls back to the active paygentContext user.
model Model name for guard checks + cost.
provider "openai" (default), "anthropic", or a custom ProviderDescriptor.
stream true if the call returns a stream to wrap.
messages Request messages — enables pre-call input-token estimation.
maxTokens Caller's max_tokens — output bound for estimation + reservation.
toolCalls, metadata Optional extras attached to the event.
const res = await pg.wrap(
  () => openai.chat.completions.create({ model: "gpt-4o", messages }),
  { userId: "user_123", model: "gpt-4o" },
);

autoInstrument (global patch)

Pass autoInstrument: true to init() to patch the installed openai / @anthropic-ai/sdk prototypes so every client instance is metered without instrument(). It must run before the .create() call. close() restores the originals. Opt-in because it mutates shared globals.


Callbacks

All return void; register as many as you like (they fire in order; one throwing doesn't stop the others).

pg.onSoftGate((g: GuardResult) => void)     // approaching a limit (call still runs)
pg.onHardGate((g: GuardResult) => void)     // over a limit; fires THEN PaygentLimitExceeded throws
pg.onUsage((e: UsageEvent) => void)         // after every successful metered call
pg.onSessionStart((s: UserState) => void)   // first time a user's state is loaded
pg.onSoftGate((g) => console.warn(g.message));
pg.onUsage((e) => metrics.add(e.costTotal));

Queries & pre-flight

pg.getUsage(userId)

getUsage(userId: string): UsageSnapshot

Reads the in-memory cache (synced with the backend). UsageSnapshot: { userId, totalCost, totalTokens, tokensByModel, costByModel }.

pg.getMaxTokens(userId, model, opts?)

getMaxTokens(
  userId: string,
  model: string,
  opts?: { estimatedInputTokens?: number; messages?: unknown; hardCap?: number },
): Promise<MaxTokensAdvice>

Recommends the largest max_tokens that won't push the user past any hard gate. Pass messages to estimate input tokens automatically. Fail-closed: any error returns { maxTokens: 0, bindingLimit: "blocked", ... }. hardCap defaults to 4096.

MaxTokensAdvice: { maxTokens, bindingLimit, periodSpendRemaining, sessionSpendRemaining, modelTokensRemaining } where bindingLimit is "period_spend" | "session_spend" | "model_tokens" | "unbounded" | "blocked".

pg.precheck(userId, model?)

precheck(userId: string, model?: string | null): Promise<GuardResult>

Runs the guard a call would run — fires soft/hard callbacks and emits the gate-audit event — but never throws and makes no LLM call. Use it for pre-flight checks and as the primitive behind framework integrations.

pg.recordUsage(userId, info, opts?)

recordUsage(userId: string, info: TokenInfo, opts?: { metadata?: Record<string, unknown> }): void

Meter a call the SDK didn't intercept (the primitive framework callbacks use after the framework reports usage). TokenInfo: { model, inputTokens, outputTokens, totalTokens }.


Errors

PaygentLimitExceeded

class PaygentLimitExceeded extends Error {
  readonly guardResult: GuardResult;
}

The only error Paygent throws — raised at a hard gate, before the LLM call runs. Everything else is fail-open. Catch it in your request handler and read err.guardResult.

try {
  await openai.chat.completions.create({ /* ... */ });
} catch (err) {
  if (err instanceof PaygentLimitExceeded) {
    err.guardResult.gateReason; // "total_spend" | "session_spend" | "model_limit:gpt-4o"
    err.guardResult.message;
    err.guardResult.usagePct;
  }
}

Framework integrations

PaygentCallbackHandler (LangChain.js)

import { PaygentCallbackHandler } from "@paygentjs/sdk/integrations/langchain";

new PaygentCallbackHandler(pg, { userId?, model?, metadata? })

A BaseCallbackHandler that guards on handleLLMStart (throws on hard gate) and meters on handleLLMEnd. Pass it via config.callbacks. Also covers LangGraph.js. Do not also instrument() the same client — the JS handler doesn't auto-deduplicate.

pg.aiMiddleware(options?) (Vercel AI SDK)

aiMiddleware(options?: { userId?: string; model?: string; metadata?: Record<string, unknown> }): LanguageModelMiddleware

A wrapLanguageModel middleware that guards + meters both generateText and streamText.

import { wrapLanguageModel } from "ai";
const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: pg.aiMiddleware({ userId }) });

Providers (advanced)

createDefaultRegistry() returns a ProviderRegistry with OPENAI_PROVIDER + ANTHROPIC_PROVIDER registered. customProvider(...) builds a ProviderDescriptor for wrap() when you need to meter a client the built-ins don't cover. Most apps never touch these.


Types

See the import block at the top of this page. Internal models are camelCase (inputTokens, costTotal, gateReason, periodTokensByModel, …) — the wire layer maps them to/from the backend's snake_case. Notable shapes:

  • GuardResult{ status, gateReason, usagePct, currentValue, limitValue, message }; status is "ok" | "soft_gate" | "hard_gate".
  • UsageEvent{ id, userId, sessionId, timestamp, model, inputTokens, outputTokens, totalTokens, toolCalls, costTokens, costTools, costTotal, metadata, synced }.
  • PlanConfig{ maxSpendPerPeriod, maxSpendPerSession, softGateAt, hardGateAt, modelLimits, costRates, defaultCostRate, preCallEstimate, preCallBufferTokens, reservationSafetyFactor, ... }.

See also