@closeloop/sdk 0.1.7 → 0.1.8
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 +18 -234
- package/dist/index.d.mts +843 -2
- package/dist/index.d.ts +843 -2
- package/dist/index.js +107 -105
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +107 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -19
- package/dist/errors-CNnLzjDZ.d.mts +0 -901
- package/dist/errors-CNnLzjDZ.d.ts +0 -901
- package/dist/nextjs.d.mts +0 -135
- package/dist/nextjs.d.ts +0 -135
- package/dist/nextjs.js +0 -952
- package/dist/nextjs.js.map +0 -1
- package/dist/nextjs.mjs +0 -917
- package/dist/nextjs.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ const client = new CloseLoop({
|
|
|
28
28
|
// Verify credits before processing
|
|
29
29
|
const verification = await client.credits.verify({
|
|
30
30
|
walletAddress: "0x1234...",
|
|
31
|
-
|
|
31
|
+
productId: "prod_01",
|
|
32
32
|
amount: 1
|
|
33
33
|
})
|
|
34
34
|
|
|
@@ -38,7 +38,7 @@ if (verification.hasEnoughCredits) {
|
|
|
38
38
|
// Consume the credit
|
|
39
39
|
await client.credits.consume({
|
|
40
40
|
walletAddress: "0x1234...",
|
|
41
|
-
|
|
41
|
+
productId: "prod_01",
|
|
42
42
|
amount: 1,
|
|
43
43
|
consumedBy: "my-service"
|
|
44
44
|
})
|
|
@@ -51,7 +51,6 @@ if (verification.hasEnoughCredits) {
|
|
|
51
51
|
- ⚡ **Fast** - Lightweight with minimal dependencies
|
|
52
52
|
- 🔄 **Atomic operations** - Verify and consume in one call
|
|
53
53
|
- 🪝 **Webhook support** - Secure signature verification with schema validation
|
|
54
|
-
- 📦 **Framework helpers** - Next.js middleware with rate limiting docs
|
|
55
54
|
- ✅ **Validated** - All inputs validated with Zod schemas
|
|
56
55
|
|
|
57
56
|
## API Reference
|
|
@@ -65,12 +64,12 @@ Check if a user has enough credits without consuming them:
|
|
|
65
64
|
```typescript
|
|
66
65
|
const result = await client.credits.verify({
|
|
67
66
|
walletAddress: "0x...",
|
|
68
|
-
|
|
67
|
+
productId: "prod_id",
|
|
69
68
|
amount: 10
|
|
70
69
|
})
|
|
71
70
|
|
|
72
|
-
console.log(result.
|
|
73
|
-
console.log(result.
|
|
71
|
+
console.log(result.hasEnough) // true
|
|
72
|
+
console.log(result.totalRemaining) // 100
|
|
74
73
|
console.log(result.expiresAt) // "2024-12-31T23:59:59Z" or null
|
|
75
74
|
```
|
|
76
75
|
|
|
@@ -81,7 +80,7 @@ Deduct credits from a user's balance:
|
|
|
81
80
|
```typescript
|
|
82
81
|
const result = await client.credits.consume({
|
|
83
82
|
walletAddress: "0x...",
|
|
84
|
-
|
|
83
|
+
productId: "prod_id",
|
|
85
84
|
amount: 1,
|
|
86
85
|
consumedBy: "ai-generation", // Optional: track what used the credits
|
|
87
86
|
metadata: { requestId: "req_123" }, // Optional: attach metadata
|
|
@@ -89,7 +88,7 @@ const result = await client.credits.consume({
|
|
|
89
88
|
})
|
|
90
89
|
|
|
91
90
|
console.log(result.success) // true
|
|
92
|
-
console.log(result.
|
|
91
|
+
console.log(result.totalRemaining) // 99
|
|
93
92
|
console.log(result.transactionId) // "tx_abc123"
|
|
94
93
|
```
|
|
95
94
|
|
|
@@ -103,7 +102,7 @@ import { InsufficientCreditsError, CreditsExpiredError } from "@closeloop/sdk"
|
|
|
103
102
|
try {
|
|
104
103
|
const result = await client.credits.verifyAndConsume({
|
|
105
104
|
walletAddress: "0x...",
|
|
106
|
-
|
|
105
|
+
productId: "prod_id",
|
|
107
106
|
amount: 5,
|
|
108
107
|
consumedBy: "batch-processing"
|
|
109
108
|
})
|
|
@@ -117,22 +116,6 @@ try {
|
|
|
117
116
|
}
|
|
118
117
|
```
|
|
119
118
|
|
|
120
|
-
#### Batch Consume
|
|
121
|
-
|
|
122
|
-
Consume credits for multiple users in one request:
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
const result = await client.credits.batchConsume({
|
|
126
|
-
operations: [
|
|
127
|
-
{ walletAddress: "0x1234...", planId: "plan_a", amount: 1 },
|
|
128
|
-
{ walletAddress: "0x5678...", planId: "plan_b", amount: 2 }
|
|
129
|
-
],
|
|
130
|
-
atomic: false // Allow partial success
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
console.log(`Success: ${result.successCount}, Failed: ${result.failedCount}`)
|
|
134
|
-
```
|
|
135
|
-
|
|
136
119
|
### Balance Queries
|
|
137
120
|
|
|
138
121
|
#### Get Specific Balance
|
|
@@ -140,7 +123,7 @@ console.log(`Success: ${result.successCount}, Failed: ${result.failedCount}`)
|
|
|
140
123
|
```typescript
|
|
141
124
|
const balance = await client.balances.get({
|
|
142
125
|
walletAddress: "0x...",
|
|
143
|
-
|
|
126
|
+
productId: "prod_id"
|
|
144
127
|
})
|
|
145
128
|
|
|
146
129
|
if (balance) {
|
|
@@ -159,8 +142,9 @@ const { balances, nextCursor, totalCount } = await client.balances.list({
|
|
|
159
142
|
limit: 10
|
|
160
143
|
})
|
|
161
144
|
|
|
145
|
+
|
|
162
146
|
for (const balance of balances) {
|
|
163
|
-
console.log(`${balance.
|
|
147
|
+
console.log(`${balance.productName}: ${balance.remainingCredits} credits`)
|
|
164
148
|
}
|
|
165
149
|
|
|
166
150
|
// Paginate
|
|
@@ -189,12 +173,14 @@ for (const tx of transactions) {
|
|
|
189
173
|
#### Get Aggregated Stats
|
|
190
174
|
|
|
191
175
|
```typescript
|
|
192
|
-
const stats = await client.balances.stats(
|
|
176
|
+
const stats = await client.balances.stats({
|
|
177
|
+
walletAddress: "0x...",
|
|
178
|
+
productId: "prod_id"
|
|
179
|
+
})
|
|
193
180
|
|
|
194
181
|
console.log(`Total: ${stats.totalCredits}`)
|
|
195
182
|
console.log(`Used: ${stats.totalUsed}`)
|
|
196
183
|
console.log(`Remaining: ${stats.totalRemaining}`)
|
|
197
|
-
console.log(`Active Balances: ${stats.activeBalances}`)
|
|
198
184
|
```
|
|
199
185
|
|
|
200
186
|
### Webhook Verification
|
|
@@ -213,8 +199,8 @@ app.post("/webhook", (req, res) => {
|
|
|
213
199
|
|
|
214
200
|
// Type-safe event handling
|
|
215
201
|
if (client.webhooks.isPaymentSuccess(event)) {
|
|
216
|
-
const { type, walletAddress,
|
|
217
|
-
console.log(`${walletAddress} purchased plan ${
|
|
202
|
+
const { type, walletAddress, productId, amount } = event.data
|
|
203
|
+
console.log(`${walletAddress} purchased plan ${productId} for $${amount} (${type})`)
|
|
218
204
|
}
|
|
219
205
|
|
|
220
206
|
if (client.webhooks.isCreditsLow(event)) {
|
|
@@ -234,134 +220,6 @@ app.post("/webhook", (req, res) => {
|
|
|
234
220
|
})
|
|
235
221
|
```
|
|
236
222
|
|
|
237
|
-
```typescript
|
|
238
|
-
// Next.js App Router
|
|
239
|
-
export async function POST(request: Request) {
|
|
240
|
-
const payload = await request.text()
|
|
241
|
-
const signature = request.headers.get("x-closeloop-signature")!
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const event = client.webhooks.verify({
|
|
245
|
-
payload,
|
|
246
|
-
signature,
|
|
247
|
-
secret: process.env.WEBHOOK_SECRET!
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
// Handle event...
|
|
251
|
-
return Response.json({ received: true })
|
|
252
|
-
} catch {
|
|
253
|
-
return Response.json({ error: "Invalid signature" }, { status: 400 })
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## Next.js Integration
|
|
259
|
-
|
|
260
|
-
### Proxy (Next.js 16+)
|
|
261
|
-
|
|
262
|
-
Protect routes by verifying credits:
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// proxy.ts (Next.js 16+)
|
|
266
|
-
import { NextRequest, NextResponse } from "next/server"
|
|
267
|
-
import { CloseLoop } from "@closeloop/sdk"
|
|
268
|
-
|
|
269
|
-
const client = new CloseLoop({ apiKey: process.env.CLOSELOOP_API_KEY! })
|
|
270
|
-
|
|
271
|
-
// Define routes that require credit verification
|
|
272
|
-
const creditProtectedRoutes = ["/api/ai", "/api/generate"]
|
|
273
|
-
|
|
274
|
-
export default async function proxy(req: NextRequest) {
|
|
275
|
-
const path = req.nextUrl.pathname
|
|
276
|
-
const isCreditProtectedRoute = creditProtectedRoutes.some(route =>
|
|
277
|
-
path.startsWith(route)
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
if (!isCreditProtectedRoute) {
|
|
281
|
-
return NextResponse.next()
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Get wallet address from header or session
|
|
285
|
-
const walletAddress = req.headers.get("x-wallet-address")
|
|
286
|
-
|
|
287
|
-
if (!walletAddress) {
|
|
288
|
-
return NextResponse.json(
|
|
289
|
-
{ error: "Wallet address required" },
|
|
290
|
-
{ status: 401 }
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Verify credits before allowing the request
|
|
295
|
-
const verification = await client.credits.verify({
|
|
296
|
-
walletAddress,
|
|
297
|
-
planId: "plan_abc123",
|
|
298
|
-
amount: 1
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
if (!verification.hasEnoughCredits) {
|
|
302
|
-
return NextResponse.json(
|
|
303
|
-
{ error: "Insufficient credits" },
|
|
304
|
-
{ status: 402 }
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return NextResponse.next()
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export const config = {
|
|
312
|
-
matcher: ["/((?!_next/static|_next/image|.*\\.png$).*)"]
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
### Route Handler
|
|
317
|
-
|
|
318
|
-
Verify and consume credits atomically in API routes:
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
// app/api/ai/route.ts (Next.js 16+)
|
|
322
|
-
import { NextRequest, NextResponse } from "next/server"
|
|
323
|
-
import { CloseLoop, InsufficientCreditsError } from "@closeloop/sdk"
|
|
324
|
-
|
|
325
|
-
const client = new CloseLoop({ apiKey: process.env.CLOSELOOP_API_KEY! })
|
|
326
|
-
|
|
327
|
-
export async function POST(request: NextRequest) {
|
|
328
|
-
const wallet = request.headers.get("x-wallet-address")
|
|
329
|
-
|
|
330
|
-
if (!wallet) {
|
|
331
|
-
return NextResponse.json(
|
|
332
|
-
{ error: "Wallet address required" },
|
|
333
|
-
{ status: 401 }
|
|
334
|
-
)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Verify and consume credits atomically
|
|
338
|
-
try {
|
|
339
|
-
const result = await client.credits.verifyAndConsume({
|
|
340
|
-
walletAddress: wallet,
|
|
341
|
-
planId: "plan_abc123",
|
|
342
|
-
amount: 1,
|
|
343
|
-
consumedBy: "ai-text-generation"
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
// Process request after credit verification
|
|
347
|
-
const aiResult = await processAIRequest()
|
|
348
|
-
|
|
349
|
-
return NextResponse.json({
|
|
350
|
-
...aiResult,
|
|
351
|
-
remainingCredits: result.remainingCredits
|
|
352
|
-
})
|
|
353
|
-
} catch (error) {
|
|
354
|
-
if (error instanceof InsufficientCreditsError) {
|
|
355
|
-
return NextResponse.json(
|
|
356
|
-
{ error: "Insufficient credits", remaining: error.remainingCredits },
|
|
357
|
-
{ status: 402 }
|
|
358
|
-
)
|
|
359
|
-
}
|
|
360
|
-
throw error
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
```
|
|
364
|
-
|
|
365
223
|
## Error Handling
|
|
366
224
|
|
|
367
225
|
All errors extend `CloseLoopError`:
|
|
@@ -416,81 +274,10 @@ try {
|
|
|
416
274
|
All inputs are validated using Zod schemas before being sent to the API:
|
|
417
275
|
|
|
418
276
|
- **Wallet addresses**: Must be valid Ethereum addresses (`0x` + 40 hex chars)
|
|
419
|
-
- **
|
|
277
|
+
- **Product IDs**: Non-empty strings with max 100 characters
|
|
420
278
|
- **Credit amounts**: Positive integers up to 1 billion
|
|
421
279
|
- **Metadata**: Sanitized to prevent prototype pollution attacks
|
|
422
280
|
|
|
423
|
-
### Rate Limiting
|
|
424
|
-
|
|
425
|
-
The proxy does **not** include rate limiting. We strongly recommend combining it with a rate limiter:
|
|
426
|
-
|
|
427
|
-
```typescript
|
|
428
|
-
// proxy.ts - With rate limiting (RECOMMENDED)
|
|
429
|
-
import { NextRequest, NextResponse } from "next/server"
|
|
430
|
-
import { CloseLoop } from "@closeloop/sdk"
|
|
431
|
-
import { Ratelimit } from "@upstash/ratelimit"
|
|
432
|
-
import { Redis } from "@upstash/redis"
|
|
433
|
-
|
|
434
|
-
const client = new CloseLoop({ apiKey: process.env.CLOSELOOP_API_KEY! })
|
|
435
|
-
|
|
436
|
-
const ratelimit = new Ratelimit({
|
|
437
|
-
redis: Redis.fromEnv(),
|
|
438
|
-
limiter: Ratelimit.slidingWindow(10, "10 s"),
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
const creditProtectedRoutes = ["/api/ai", "/api/generate"]
|
|
442
|
-
|
|
443
|
-
export default async function proxy(request: NextRequest) {
|
|
444
|
-
const path = request.nextUrl.pathname
|
|
445
|
-
const isCreditProtectedRoute = creditProtectedRoutes.some(route =>
|
|
446
|
-
path.startsWith(route)
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
if (!isCreditProtectedRoute) {
|
|
450
|
-
return NextResponse.next()
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Rate limit by IP first
|
|
454
|
-
const ip = request.headers.get("x-forwarded-for") ?? "127.0.0.1"
|
|
455
|
-
const { success } = await ratelimit.limit(ip)
|
|
456
|
-
|
|
457
|
-
if (!success) {
|
|
458
|
-
return NextResponse.json(
|
|
459
|
-
{ error: "Rate limit exceeded" },
|
|
460
|
-
{ status: 429 }
|
|
461
|
-
)
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Then verify credits
|
|
465
|
-
const walletAddress = request.headers.get("x-wallet-address")
|
|
466
|
-
if (!walletAddress) {
|
|
467
|
-
return NextResponse.json(
|
|
468
|
-
{ error: "Wallet address required" },
|
|
469
|
-
{ status: 401 }
|
|
470
|
-
)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const verification = await client.credits.verify({
|
|
474
|
-
walletAddress,
|
|
475
|
-
planId: "plan_abc123",
|
|
476
|
-
amount: 1
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
if (!verification.hasEnoughCredits) {
|
|
480
|
-
return NextResponse.json(
|
|
481
|
-
{ error: "Insufficient credits" },
|
|
482
|
-
{ status: 402 }
|
|
483
|
-
)
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
return NextResponse.next()
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
export const config = {
|
|
490
|
-
matcher: ["/((?!_next/static|_next/image|.*\\.png$).*)"]
|
|
491
|
-
}
|
|
492
|
-
```
|
|
493
|
-
|
|
494
281
|
### Webhook Security
|
|
495
282
|
|
|
496
283
|
- HMAC-SHA256 signatures with timing-safe comparison
|
|
@@ -511,8 +298,6 @@ import type {
|
|
|
511
298
|
VerifyCreditsResponse,
|
|
512
299
|
ConsumeCreditsParams,
|
|
513
300
|
ConsumeCreditsResponse,
|
|
514
|
-
BatchConsumeParams,
|
|
515
|
-
BatchConsumeResponse,
|
|
516
301
|
|
|
517
302
|
// Balances
|
|
518
303
|
CreditBalance,
|
|
@@ -553,7 +338,6 @@ const client = new CloseLoop({
|
|
|
553
338
|
## Requirements
|
|
554
339
|
|
|
555
340
|
- Node.js 18.0.0 or higher
|
|
556
|
-
- Next.js 14.0.0 or higher (for Next.js helpers)
|
|
557
341
|
|
|
558
342
|
## License
|
|
559
343
|
|