@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 +140 -23
- package/dist/index.cjs +602 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +915 -62
- package/dist/index.d.ts +915 -62
- package/dist/index.js +603 -58
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
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
|
|
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
|
-
|
|
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 `
|
|
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
|
|
50
|
-
await cnk.signals.credit({ customerId, model: "llama-3.3-70b-versatile",
|
|
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 (
|
|
56
|
-
await cnk.signals.outcome({ customerId, model: "llama-3.3-70b-versatile",
|
|
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
|
|
67
|
-
on a size/interval trigger. This keeps billing telemetry off your LLM hot
|
|
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`.
|
|
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://
|
|
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
|
|
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://
|
|
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
|