@clocknext/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @clocknext/sdk
2
+
3
+ Official ClockNext SDK for Node/TypeScript. Record **usage signals** and manage
4
+ **customers** from your backend.
5
+
6
+ > **Server-side only.** Your `cnk_…` API key has org-wide write access — never
7
+ > ship it to a browser. For embedding the customer portal in a browser, mint a
8
+ > short-lived token with `cnk.portal.createToken()`.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @clocknext/sdk
14
+ ```
15
+
16
+ Requires Node ≥ 18 (native `fetch`). Zero runtime dependencies.
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import { ClockNext } from "@clocknext/sdk";
22
+
23
+ const cnk = new ClockNext({ apiKey: process.env.CLOCKNEXT_API_KEY! });
24
+
25
+ await cnk.signals.credit({
26
+ customerId: "cus_123",
27
+ model: "llama-3.3-70b-versatile", // catalog model id (OrgModel.modelId)
28
+ key: "api_credit", // the credit's stable key
29
+ member: "alice@acme.com",
30
+ tokens: { input: 1200, output: 340, cache: 0 },
31
+ custom: { feature: "summarize" },
32
+ });
33
+
34
+ // Before a serverless function returns (or on shutdown):
35
+ await cnk.flush();
36
+ ```
37
+
38
+ ## Recording signals
39
+
40
+ There are three signal types, matching ClockNext's billable primitives. Each has
41
+ a typed helper that enforces the right required fields at compile time.
42
+
43
+ `model` is always the **catalog model id** enabled in your org (matched
44
+ case-insensitively against `OrgModel.modelId`). Credits and outcomes are
45
+ referenced by their **stable `key`** (assigned when you create the credit /
46
+ outcome step), not by display name.
47
+
48
+ ```ts
49
+ // Credit — metered against a credit (by key); charged at the model's real cost + your margin
50
+ await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", key: "api_credit", tokens: { input: 1200, output: 340 } });
51
+
52
+ // Wallet — debits the customer's USD wallet at model cost
53
+ await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });
54
+
55
+ // Outcome — advances one step of a workflow (key = the OutcomeStep's key)
56
+ await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", key: "resolved_ticket.triage" });
57
+ ```
58
+
59
+ > Usage `/api/v1/usage` responses use the envelope
60
+ > `{ statusCode, statusDetail, result }`; customer endpoints currently use
61
+ > `{ ok, … }`. The SDK unwraps both for you.
62
+
63
+ ### Async vs sync
64
+
65
+ By default the client is in **`async`** mode: `track()` buffers the signal and
66
+ returns immediately (`{ queued: true }`), and a background flusher sends batches
67
+ on a size/interval trigger. This keeps billing telemetry off your LLM hot path.
68
+
69
+ To get the computed result back (and any real-time allowance rejection), either:
70
+
71
+ ```ts
72
+ // per call
73
+ const res = await cnk.signals.credit({ … }, { wait: true });
74
+ console.log(res.usageLog);
75
+
76
+ // or globally
77
+ const cnk = new ClockNext({ apiKey, mode: "sync" });
78
+ ```
79
+
80
+ > In `async` mode, server-side rejections (e.g. insufficient allowance, `422`)
81
+ > surface via the `onError` hook or `flush()`, not from the `track()` call.
82
+
83
+ ### Reliability
84
+
85
+ - **Idempotency:** every signal carries an idempotency key (auto-generated once,
86
+ reused across retries) so a transport retry can't double-count. Pass your own
87
+ `idempotencyKey` for deterministic, cross-process dedup. *(Server-side dedup
88
+ enforcement is rolling out — see the SDK plan.)*
89
+ - **Retries:** transient failures (network, timeout, `408/409/429/5xx`) are
90
+ retried with exponential backoff + jitter, honoring `Retry-After`. Deterministic
91
+ `4xx` (`400/401/404/422`) are never retried.
92
+ - **Flushing:** call `await cnk.flush()` before a serverless return, and
93
+ `await cnk.close()` on graceful shutdown to drain the buffer.
94
+
95
+ ## Customers
96
+
97
+ ```ts
98
+ const c = await cnk.customers.create({ name: "Acme", email: "ops@acme.com" });
99
+ const one = await cnk.customers.get(c.id);
100
+ const page = await cnk.customers.list({ limit: 50, q: "ac" });
101
+ await cnk.customers.update(c.id, { notes: "VIP" });
102
+ await cnk.customers.delete(c.id);
103
+
104
+ // iterate every customer (auto-follows the cursor)
105
+ for await (const cust of cnk.customers.iterate()) { /* … */ }
106
+
107
+ // insights
108
+ await cnk.customers.usage(c.id, { limit: 50 });
109
+ await cnk.customers.balances(c.id);
110
+ await cnk.customers.plan(c.id);
111
+ await cnk.customers.revenue(c.id, { range: "30d" });
112
+ ```
113
+
114
+ ## Customer portal token
115
+
116
+ ```ts
117
+ const { token, expiresAt } = await cnk.portal.createToken({ customerId: "cus_123", ttlSeconds: 600 });
118
+ // render <iframe src={`https://app.clocknext.com/portal/embed?token=${token}`} />
119
+ ```
120
+
121
+ ## Errors
122
+
123
+ All failures throw typed errors you can branch on:
124
+
125
+ ```ts
126
+ import { AllowanceError, AuthError, RateLimitError } from "@clocknext/sdk";
127
+
128
+ try {
129
+ await cnk.signals.credit({ … }, { wait: true });
130
+ } catch (err) {
131
+ if (err instanceof AllowanceError) {/* upsell */}
132
+ else if (err instanceof AuthError) {/* bad key */}
133
+ else if (err instanceof RateLimitError) {/* back off */}
134
+ }
135
+ ```
136
+
137
+ `ValidationError (400)` · `AuthError (401)` · `NotFoundError (404)` ·
138
+ `PlanError (422)` · `AllowanceError (422)` · `ConflictError (409)` ·
139
+ `RateLimitError (429)` · `ServerError (5xx)` · `NetworkError` — all extend
140
+ `ClockNextError` (`.status`, `.retryable`, `.idempotencyKey`).
141
+
142
+ ## Configuration
143
+
144
+ ```ts
145
+ new ClockNext({
146
+ apiKey: "cnk_…", // required
147
+ baseUrl: "https://app.clocknext.com",
148
+ mode: "async", // "async" | "sync"
149
+ timeoutMs: 10_000,
150
+ batch: { maxSize: 20, maxIntervalMs: 2000, maxConcurrency: 5, maxQueueSize: 10_000 },
151
+ retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10_000 },
152
+ onError: (err, signal) => {},
153
+ onFlush: (count) => {},
154
+ onRetry: ({ attempt, delayMs }) => {},
155
+ onDrop: (signal, reason) => {},
156
+ });
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT