@clocknext/sdk 0.1.0 → 0.4.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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # @clocknext/sdk
2
2
 
3
3
  Official ClockNext SDK for Node/TypeScript. Record **usage signals** and manage
4
- **customers** from your backend.
4
+ **plans, purchases, invoices, customers, catalogs, and billing** from your
5
+ backend — full coverage of the ClockNext `/api/v1` surface.
5
6
 
6
7
  > **Server-side only.** Your `cnk_…` API key has org-wide write access — never
7
8
  > ship it to a browser. For embedding the customer portal in a browser, mint a
@@ -13,7 +14,9 @@ Official ClockNext SDK for Node/TypeScript. Record **usage signals** and manage
13
14
  npm install @clocknext/sdk
14
15
  ```
15
16
 
16
- Requires Node ≥ 18 (native `fetch`). Zero runtime dependencies.
17
+ Requires Node ≥ 18 (native `fetch`). Zero runtime dependencies. Uses only
18
+ Web-standard `fetch` and `crypto`, so it also runs on edge runtimes (Cloudflare
19
+ Workers, Vercel Edge, Deno, Bun).
17
20
 
18
21
  ## Quick start
19
22
 
@@ -25,7 +28,7 @@ const cnk = new ClockNext({ apiKey: process.env.CLOCKNEXT_API_KEY! });
25
28
  await cnk.signals.credit({
26
29
  customerId: "cus_123",
27
30
  model: "llama-3.3-70b-versatile", // catalog model id (OrgModel.modelId)
28
- key: "api_credit", // the credit's stable key
31
+ agentKey: "api_credit", // the credit's stable agent key
29
32
  member: "alice@acme.com",
30
33
  tokens: { input: 1200, output: 340, cache: 0 },
31
34
  custom: { feature: "summarize" },
@@ -42,29 +45,31 @@ a typed helper that enforces the right required fields at compile time.
42
45
 
43
46
  `model` is always the **catalog model id** enabled in your org (matched
44
47
  case-insensitively against `OrgModel.modelId`). Credits and outcomes are
45
- referenced by their **stable `key`** (assigned when you create the credit /
48
+ referenced by their **stable `agentKey`** (assigned when you create the credit /
46
49
  outcome step), not by display name.
47
50
 
48
51
  ```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 } });
52
+ // Credit — metered against a credit (by agentKey); charged at the model's real cost + your margin
53
+ await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile", agentKey: "api_credit", tokens: { input: 1200, output: 340 } });
51
54
 
52
55
  // Wallet — debits the customer's USD wallet at model cost
53
56
  await cnk.signals.wallet({ customerId, model: "openai/gpt-oss-120b", tokens: { input: 800, output: 5000 } });
54
57
 
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" });
58
+ // Outcome — advances one step of a workflow (agentKey = the OutcomeStep's agent key)
59
+ await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile", agentKey: "resolved_ticket.triage" });
57
60
  ```
58
61
 
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
62
  ### Async vs sync
64
63
 
65
64
  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.
65
+ returns immediately (`{ queued: true }`), and a background flusher drains the
66
+ buffer on a size/interval trigger. This keeps billing telemetry off your LLM hot
67
+ path.
68
+
69
+ > **"Batching" here means timing, not a bulk request.** Each signal is still its
70
+ > own `POST /api/v1/usage`; the flusher just groups *when* sends happen and caps
71
+ > how many run at once (`batch.maxConcurrency`). There is no single bulk-ingest
72
+ > call, so size your request budget per signal.
68
73
 
69
74
  To get the computed result back (and any real-time allowance rejection), either:
70
75
 
@@ -82,15 +87,27 @@ const cnk = new ClockNext({ apiKey, mode: "sync" });
82
87
 
83
88
  ### Reliability
84
89
 
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
90
  - **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.
91
+ retried with exponential backoff + equal jitter, honoring `Retry-After`.
92
+ Deterministic `4xx` (`400/401/404/422`) are never retried.
93
+ - **Replay safety:** reads (`GET`) and `DELETE` are retried automatically. Metering
94
+ signals (`signals.*`) opt into retries too — made safe by the idempotency key
95
+ below. Other non-idempotent writes — e.g. `customers.create`, `plans.create`,
96
+ `purchases.create` — are *not* auto-retried, so a transient blip can never
97
+ create a duplicate.
98
+ - **Idempotency:** every metering signal carries an idempotency key — auto-
99
+ generated per signal, or pass your own via `idempotencyKey` on the signal —
100
+ reused across all retries, so a retried send is **deduplicated by the server**
101
+ instead of double-counted. Pass a stable key (e.g. your own event id) to also
102
+ dedup across process restarts or at-least-once redelivery.
92
103
  - **Flushing:** call `await cnk.flush()` before a serverless return, and
93
104
  `await cnk.close()` on graceful shutdown to drain the buffer.
105
+ - **Durability:** the async buffer is **in-memory**. A signal already accepted
106
+ by the server is durable on the server side, but signals still buffered when a
107
+ process crashes (before send) are lost. For zero-loss-before-receipt, either
108
+ run in `sync` mode (or `{ wait: true }`) so the send completes before your
109
+ code continues, or call `flush()`/`close()` at the right boundaries. A
110
+ pluggable before-send persistence store is not yet available.
94
111
 
95
112
  ## Customers
96
113
 
@@ -111,11 +128,108 @@ await cnk.customers.plan(c.id);
111
128
  await cnk.customers.revenue(c.id, { range: "30d" });
112
129
  ```
113
130
 
131
+ ### Members
132
+
133
+ ```ts
134
+ await cnk.customers.addMember(c.id, { name: "Alice", role: "ADMIN", email: "alice@acme.com" });
135
+ const members = await cnk.customers.listMembers(c.id);
136
+ await cnk.customers.updateMember(c.id, members[0].id, { name: "Alice R." });
137
+ await cnk.customers.removeMember(c.id, members[0].id);
138
+ ```
139
+
140
+ ### Wallet & billing config
141
+
142
+ ```ts
143
+ // wallet
144
+ await cnk.customers.wallet(c.id, { limit: 50 }); // { balanceUsd, transactions }
145
+ await cnk.customers.addWalletEntry(c.id, { type: "CREDIT", amount: 25 }); // top up (DEBIT to spend)
146
+
147
+ // billing config — currency is set through update()
148
+ await cnk.customers.update(c.id, { currencyCode: "EUR" });
149
+
150
+ // grant / claw back balances (delta is signed)
151
+ await cnk.customers.adjustCredit(c.id, { creditId: "cr_1", delta: 100 });
152
+ const { remaining } = await cnk.customers.adjustOutcome(c.id, { outcomeId: "out_1", delta: -1 });
153
+ await cnk.customers.adjustUnit(c.id, { unitId: "un_1", delta: 5 });
154
+ ```
155
+
156
+ ## Plans & purchases
157
+
158
+ ```ts
159
+ // define a plan
160
+ const plan = await cnk.plans.create({
161
+ name: "Pro",
162
+ billingCycle: "MONTHLY",
163
+ currencyCode: "USD",
164
+ components: [
165
+ { type: "WALLET", billingMode: "ADVANCE", amount: 100 },
166
+ { type: "CREDIT", billingMode: "ADVANCE", creditId: "cr_1", quantity: 10_000 },
167
+ ],
168
+ });
169
+ await cnk.plans.list({ active: true });
170
+ await cnk.plans.setActive(plan.id, false);
171
+
172
+ // subscribe a customer (auto-cancels their existing active/scheduled purchase)
173
+ const purchase = await cnk.purchases.create({ customerId: c.id, planId: plan.id });
174
+ await cnk.purchases.setAutoPayment(purchase.id, false, { pauseReason: "trial" });
175
+ await cnk.purchases.cancel(purchase.id);
176
+ ```
177
+
178
+ ## Invoices & payments (read-only)
179
+
180
+ ```ts
181
+ const invoices = await cnk.invoices.list({ customerId: c.id, status: "OPEN" });
182
+ const { invoice, plan } = await cnk.invoices.get(invoices[0].id);
183
+ const { url } = await cnk.invoices.payLink(invoices[0].id); // hosted pay link (OPEN only)
184
+
185
+ const payments = await cnk.payments.list({ customerId: c.id });
186
+ await cnk.payments.get(payments[0].id); // id = the paid invoice id
187
+ ```
188
+
189
+ ## Catalogs: credits, outcomes, units
190
+
191
+ Define the billable *types*; record usage against them with `signals.*`.
192
+
193
+ ```ts
194
+ // credit type — signals.credit({ agentKey }) meters against it
195
+ await cnk.credits.create({ name: "API", agentKey: "api_credit", basePrice: 1, marginPercent: 20, pricePerCredit: 0.01 });
196
+ await cnk.credits.list({ active: true });
197
+
198
+ // outcome type — a multi-step workflow; each step has its own agentKey
199
+ await cnk.outcomes.create({
200
+ name: "Resolved ticket",
201
+ basePrice: 1, marginPercent: 20, pricePerOutcome: 2,
202
+ steps: [{ name: "Triage", agentKey: "resolved_ticket.triage", basePrice: 0.5 }],
203
+ });
204
+
205
+ // unit type — a quantity-priced meter (FLAT / SLAB / VOLUME)
206
+ await cnk.units.create({ name: "Seats", agentKey: "seats", pricingType: "FLAT", flatPrice: 5 });
207
+ ```
208
+
209
+ ### Unit metering
210
+
211
+ Record a unit event (no tokens) and read a customer's unit balances:
212
+
213
+ ```ts
214
+ await cnk.signals.unit({ customerId: c.id, agentKey: "seats" });
215
+ await cnk.signals.unitUsage({ customerId: c.id });
216
+ ```
217
+
218
+ ## Webhooks
219
+
220
+ Threshold alerts — at most one per kind (`CREDIT` / `OUTCOME` / `WALLET`):
221
+
222
+ ```ts
223
+ await cnk.webhooks.upsert({ kind: "WALLET", threshold: 10, url: "https://acme.com/hooks/clocknext" });
224
+ await cnk.webhooks.list();
225
+ await cnk.webhooks.delete("WALLET");
226
+ ```
227
+
114
228
  ## Customer portal token
115
229
 
116
230
  ```ts
117
231
  const { token, expiresAt } = await cnk.portal.createToken({ customerId: "cus_123", ttlSeconds: 600 });
118
- // render <iframe src={`https://app.clocknext.com/portal/embed?token=${token}`} />
232
+ // render <iframe src={`https://payments.clocknext.com/portal/embed?token=${token}`} />
119
233
  ```
120
234
 
121
235
  ## Errors
@@ -137,14 +251,14 @@ try {
137
251
  `ValidationError (400)` · `AuthError (401)` · `NotFoundError (404)` ·
138
252
  `PlanError (422)` · `AllowanceError (422)` · `ConflictError (409)` ·
139
253
  `RateLimitError (429)` · `ServerError (5xx)` · `NetworkError` — all extend
140
- `ClockNextError` (`.status`, `.retryable`, `.idempotencyKey`).
254
+ `ClockNextError` (`.status`, `.retryable`).
141
255
 
142
256
  ## Configuration
143
257
 
144
258
  ```ts
145
259
  new ClockNext({
146
260
  apiKey: "cnk_…", // required
147
- baseUrl: "https://app.clocknext.com",
261
+ baseUrl: "https://payments.clocknext.com",
148
262
  mode: "async", // "async" | "sync"
149
263
  timeoutMs: 10_000,
150
264
  batch: { maxSize: 20, maxIntervalMs: 2000, maxConcurrency: 5, maxQueueSize: 10_000 },
@@ -156,6 +270,9 @@ new ClockNext({
156
270
  });
157
271
  ```
158
272
 
273
+ Numeric options are validated: a negative or non-finite value (e.g. a bad
274
+ `timeoutMs`) falls back to its default rather than silently breaking the client.
275
+
159
276
  ## License
160
277
 
161
278
  MIT