Sign In

@clocknext/sdk

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@clocknext/sdk

Official ClockNext SDK — record usage signals and manage plans, purchases, invoices, customers, and billing from your backend.

latest
npmnpm
Version
0.6.0
Version published
Maintainers
1
Created
Source

@clocknext/sdk

Official ClockNext SDK for Node/TypeScript. Record usage signals and manage plans, purchases, invoices, customers, catalogs, and billing from your backend — full coverage of the ClockNext /api/v1 surface.

Server-side only. Your cnk_… API key has org-wide write access — never ship it to a browser. For embedding the customer portal in a browser, mint a short-lived token with cnk.portal.createToken().

Install

npm install @clocknext/sdk

Requires Node ≥ 18 (native fetch). Zero runtime dependencies. Uses only Web-standard fetch and crypto, so it also runs on edge runtimes (Cloudflare Workers, Vercel Edge, Deno, Bun).

Quick start

import { ClockNext } from "@clocknext/sdk";

const cnk = new ClockNext({ apiKey: process.env.CLOCKNEXT_API_KEY! });

await cnk.signals.credit({
  customerId: "cus_123",
  model: "llama-3.3-70b-versatile", // catalog model id (OrgModel.modelId)
  agentKey: "api_credit",           // the credit's stable agent key
  member: "alice@acme.com",
  tokens: { input: 1200, output: 340, cache: 0 },
  custom: { feature: "summarize" },
});

// Before a serverless function returns (or on shutdown):
await cnk.flush();

Recording signals

There are three signal types, matching ClockNext's billable primitives. Each has a typed helper that enforces the right required fields at compile time.

model is always the catalog model id enabled in your org (matched case-insensitively against OrgModel.modelId). Credits and outcomes are referenced by their stable agentKey (assigned when you create the credit / outcome step), not by display name.

// Credit — metered against a credit (by agentKey); charged at the model's real cost + your margin
await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit", tokens: { input: 1200, output: 340 } });

// Wallet — debits the customer's USD wallet at model cost
await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });

// Outcome — advances one step of a workflow (agentKey = the OutcomeStep's agent key)
await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", agentKey: "resolved_ticket.triage" });

Async vs sync

By default the client is in async mode: track() buffers the signal and returns immediately ({ queued: true }), and a background flusher drains the buffer on a size/interval trigger. This keeps billing telemetry off your LLM hot path.

"Batching" here means timing, not a bulk request. Each signal is still its own POST /api/v1/usage; the flusher just groups when sends happen and caps how many run at once (batch.maxConcurrency). There is no single bulk-ingest call, so size your request budget per signal.

To get the computed result back (and any real-time allowance rejection), either:

// per call
const res = await cnk.signals.credit({ … }, { wait: true });
console.log(res.usageLog);

// or globally
const cnk = new ClockNext({ apiKey, mode: "sync" });

In async mode, server-side rejections (e.g. insufficient allowance, 422) surface via the onError hook or flush(), not from the track() call.

Reliability

  • Retries: transient failures (network, timeout, 408/409/429/5xx) are retried with exponential backoff + equal jitter, honoring Retry-After. Deterministic 4xx (400/401/404/422) are never retried.
  • Replay safety: reads (GET) and DELETE are retried automatically. Metering signals (signals.*) opt into retries too — made safe by the idempotency key below. Other non-idempotent writes — e.g. customers.create, plans.create, purchases.create — are not auto-retried, so a transient blip can never create a duplicate.
  • Idempotency: every metering signal carries an idempotency key — auto- generated per signal, or pass your own via idempotencyKey on the signal — reused across all retries, so a retried send is deduplicated by the server instead of double-counted. Pass a stable key (e.g. your own event id) to also dedup across process restarts or at-least-once redelivery.
  • Flushing: call await cnk.flush() before a serverless return, and await cnk.close() on graceful shutdown to drain the buffer.
  • Durability: the async buffer is in-memory. A signal already accepted by the server is durable on the server side, but signals still buffered when a process crashes (before send) are lost. For zero-loss-before-receipt, either run in sync mode (or { wait: true }) so the send completes before your code continues, or call flush()/close() at the right boundaries. A pluggable before-send persistence store is not yet available.

Customers

const c   = await cnk.customers.create({ name: "Acme", email: "ops@acme.com" });
const one = await cnk.customers.get(c.id);
const page = await cnk.customers.list({ limit: 50, q: "ac" });
await cnk.customers.update(c.id, { notes: "VIP" });
await cnk.customers.delete(c.id);

// iterate every customer (auto-follows the cursor)
for await (const cust of cnk.customers.iterate()) { /* … */ }

// insights
await cnk.customers.usage(c.id, { limit: 50 });
await cnk.customers.balances(c.id);
await cnk.customers.plan(c.id);
await cnk.customers.revenue(c.id, { range: "30d" });

Members

await cnk.customers.addMember(c.id, { name: "Alice", role: "ADMIN", email: "alice@acme.com" });
const members = await cnk.customers.listMembers(c.id);
await cnk.customers.updateMember(c.id, members[0].id, { name: "Alice R." });
await cnk.customers.removeMember(c.id, members[0].id);

Wallet & billing config

// wallet
await cnk.customers.wallet(c.id, { limit: 50 });               // { balanceUsd, transactions }
await cnk.customers.addWalletEntry(c.id, { type: "CREDIT", amount: 25 }); // top up (DEBIT to spend)

// billing config — currency is set through update()
await cnk.customers.update(c.id, { currencyCode: "EUR" });

// grant / claw back balances (delta is signed)
await cnk.customers.adjustCredit(c.id, { creditId: "cr_1", delta: 100 });
const { remaining } = await cnk.customers.adjustOutcome(c.id, { outcomeId: "out_1", delta: -1 });
await cnk.customers.adjustUnit(c.id, { unitId: "un_1", delta: 5 });

Plans & purchases

// define a plan
const plan = await cnk.plans.create({
  name: "Pro",
  billingCycle: "MONTHLY",
  currencyCode: "USD",
  components: [
    { type: "WALLET", billingMode: "ADVANCE", amount: 100 },
    { type: "CREDIT", billingMode: "ADVANCE", creditId: "cr_1", quantity: 10_000 },
  ],
});
await cnk.plans.list({ active: true });
await cnk.plans.setActive(plan.id, false);

// subscribe a customer (auto-cancels their existing active/scheduled purchase)
const purchase = await cnk.purchases.create({ customerId: c.id, planId: plan.id });
await cnk.purchases.setAutoPayment(purchase.id, false, { pauseReason: "trial" });
await cnk.purchases.cancel(purchase.id);

Invoices & payments (read-only)

const invoices = await cnk.invoices.list({ customerId: c.id, status: "OPEN" });
const { invoice, plan } = await cnk.invoices.get(invoices[0].id);
const { url } = await cnk.invoices.payLink(invoices[0].id); // hosted pay link (OPEN only)

const payments = await cnk.payments.list({ customerId: c.id });
await cnk.payments.get(payments[0].id); // id = the paid invoice id

Catalogs: credits, outcomes, units

Define the billable types; record usage against them with signals.*.

// credit type — signals.credit({ agentKey }) meters against it
await cnk.credits.create({ name: "API", agentKey: "api_credit", basePrice: 1, marginPercent: 20, pricePerCredit: 0.01 });
await cnk.credits.list({ active: true });

// outcome type — a multi-step workflow; each step has its own agentKey
await cnk.outcomes.create({
  name: "Resolved ticket",
  basePrice: 1, marginPercent: 20, pricePerOutcome: 2,
  steps: [{ name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 }],
});

// unit type — a quantity-priced meter (FLAT / SLAB / VOLUME)
await cnk.units.create({ name: "Seats", agentKey: "seats", pricingType: "FLAT", flatPrice: 5 });

Unit metering

Record a unit event (no tokens) and read a customer's unit balances:

await cnk.signals.unit({ customerId: c.id, agentKey: "seats" });
await cnk.signals.unitUsage({ customerId: c.id });

Webhooks

Threshold alerts — at most one per kind (CREDIT / OUTCOME / WALLET):

await cnk.webhooks.upsert({ kind: "WALLET", threshold: 10, url: "https://acme.com/hooks/clocknext" });
await cnk.webhooks.list();
await cnk.webhooks.delete("WALLET");

Customer portal token

const { token, expiresAt } = await cnk.portal.createToken({ customerId: "cus_123", ttlSeconds: 600 });
// render <iframe src={`https://payments.clocknext.com/portal/embed?token=${token}`} />

Errors

All failures throw typed errors you can branch on:

import { AllowanceError, AuthError, RateLimitError } from "@clocknext/sdk";

try {
  await cnk.signals.credit({ … }, { wait: true });
} catch (err) {
  if (err instanceof AllowanceError) {/* upsell */}
  else if (err instanceof AuthError) {/* bad key */}
  else if (err instanceof RateLimitError) {/* back off */}
}

ValidationError (400) · AuthError (401) · NotFoundError (404) · PlanError (422) · AllowanceError (422) · ConflictError (409) · RateLimitError (429) · ServerError (5xx) · NetworkError — all extend ClockNextError (.status, .retryable).

Configuration

new ClockNext({
  apiKey: "cnk_…",                 // required
  baseUrl: "https://payments.clocknext.com",
  mode: "async",                    // "async" | "sync"
  timeoutMs: 10_000,
  batch: { maxSize: 20, maxIntervalMs: 2000, maxConcurrency: 5, maxQueueSize: 10_000 },
  retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10_000 },
  onError: (err, signal) => {},
  onFlush: (count) => {},
  onRetry: ({ attempt, delayMs }) => {},
  onDrop: (signal, reason) => {},
});

Numeric options are validated: a negative or non-finite value (e.g. a bad timeoutMs) falls back to its default rather than silently breaking the client.

License

MIT

Keywords

clocknext

FAQs

Package last updated on 04 Aug 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts