@closeloop/sdk
v0.1.10
Published
Official Node.js SDK for CloseLoop credit billing system
Maintainers
Readme
@closeloop/sdk
Official Node.js SDK for the CloseLoop credit billing system.
Installation
npm install @closeloop/sdk
# or
pnpm add @closeloop/sdk
# or
yarn add @closeloop/sdkQuick Start
import { CloseLoop } from "@closeloop/sdk"
const client = new CloseLoop({
apiKey: process.env.CLOSELOOP_API_KEY!
})
// Verify credits before processing
const verification = await client.credits.verify({
walletAddress: "0x1234...",
productId: "prod_01",
amount: 1
})
if (verification.hasEnoughCredits) {
// Process the request...
// Consume the credit
await client.credits.consume({
walletAddress: "0x1234...",
productId: "prod_01",
amount: 1,
consumedBy: "my-service"
})
}Features
- 🔒 Secure - Input validation, HMAC webhook verification, timing-safe comparisons
- ⚡ Fast - Lightweight with minimal dependencies
- 🔄 Atomic operations - Verify and consume in one call
- 🪝 Webhook support - Secure signature verification with schema validation
- ✅ Validated - All inputs validated with Zod schemas
API Reference
Credit Operations
Verify Credits
Check if a user has enough credits without consuming them:
const result = await client.credits.verify({
walletAddress: "0x...",
productId: "prod_id",
amount: 10
})
console.log(result.hasEnough) // true
console.log(result.totalRemaining) // 100
console.log(result.expiresAt) // "2024-12-31T23:59:59Z" or nullConsume Credits
Deduct credits from a user's balance:
const result = await client.credits.consume({
walletAddress: "0x...",
productId: "prod_id",
amount: 1,
consumedBy: "ai-generation", // Optional: track what used the credits
metadata: { requestId: "req_123" }, // Optional: attach metadata
idempotencyKey: "unique-key-123" // Optional: prevent duplicates
})
console.log(result.success) // true
console.log(result.totalRemaining) // 99
console.log(result.transactionId) // "tx_abc123"Verify and Consume (Atomic)
Verify and consume in a single operation to prevent race conditions:
import { InsufficientCreditsError, CreditsExpiredError } from "@closeloop/sdk"
try {
const result = await client.credits.verifyAndConsume({
walletAddress: "0x...",
productId: "prod_id",
amount: 5,
consumedBy: "batch-processing"
})
// Credits were verified and consumed
} catch (error) {
if (error instanceof InsufficientCreditsError) {
console.log(`Need ${error.requiredCredits}, have ${error.remainingCredits}`)
} else if (error instanceof CreditsExpiredError) {
console.log(`Credits expired at ${error.expiresAt}`)
}
}Balance Queries
Get Specific Balance
const balance = await client.balances.get({
walletAddress: "0x...",
productId: "prod_id"
})
if (balance) {
console.log(`${balance.remainingCredits} / ${balance.totalCredits} credits`)
console.log(`Active: ${balance.isActive}`)
console.log(`Expires: ${balance.expiresAt}`)
}List All Balances
const { balances, nextCursor, totalCount } = await client.balances.list({
walletAddress: "0x...",
activeOnly: true,
limit: 10
})
for (const balance of balances) {
console.log(`${balance.productName}: ${balance.remainingCredits} credits`)
}
// Paginate
if (nextCursor) {
const nextPage = await client.balances.list({
walletAddress: "0x...",
cursor: nextCursor
})
}Get Transaction History
const { transactions } = await client.balances.transactions({
balanceId: "bal_id",
type: "CONSUMPTION", // Optional filter
limit: 50
})
for (const tx of transactions) {
console.log(`${tx.type}: ${tx.amount} - ${tx.description}`)
}Get Aggregated Stats
const stats = await client.balances.stats({
walletAddress: "0x...",
productId: "prod_id"
})
console.log(`Total: ${stats.totalCredits}`)
console.log(`Used: ${stats.totalUsed}`)
console.log(`Remaining: ${stats.totalRemaining}`)Webhook Verification
Securely verify webhook signatures:
// Express
app.post("/webhook", (req, res) => {
try {
const event = client.webhooks.verify({
payload: req.body, // raw body string
signature: req.headers["x-closeloop-signature"],
secret: process.env.WEBHOOK_SECRET!
})
// Type-safe event handling
if (client.webhooks.isPaymentSuccess(event)) {
const { type, walletAddress, productId, amount } = event.data
console.log(`${walletAddress} purchased plan ${productId} for $${amount} (${type})`)
}
if (client.webhooks.isCreditsLow(event)) {
const { walletAddress, remainingCredits, threshold } = event.data
console.log(`Low balance alert: ${remainingCredits} < ${threshold}`)
}
if (client.webhooks.isCreditsExpired(event)) {
const { walletAddress, expiredCredits } = event.data
console.log(`${expiredCredits} credits expired`)
}
res.json({ received: true })
} catch (error) {
res.status(400).json({ error: "Invalid signature" })
}
})Error Handling
All errors extend CloseLoopError:
import {
CloseLoopError,
InsufficientCreditsError,
CreditsExpiredError,
AuthenticationError,
RateLimitError,
NetworkError,
NotFoundError,
ValidationError
} from "@closeloop/sdk"
try {
await client.credits.consume({ ... })
} catch (error) {
if (error instanceof ValidationError) {
// 400 - Invalid input (bad wallet address, negative amount, etc.)
console.log(`Validation error: ${error.message}`)
} else if (error instanceof InsufficientCreditsError) {
// 402 - Not enough credits
console.log(`Need ${error.requiredCredits}, have ${error.remainingCredits}`)
} else if (error instanceof CreditsExpiredError) {
// 410 - Credits expired
console.log(`Expired at ${error.expiresAt}`)
} else if (error instanceof AuthenticationError) {
// 401 - Invalid API key
console.log("Check your API key")
} else if (error instanceof RateLimitError) {
// 429 - Too many requests
console.log(`Retry after ${error.retryAfter} seconds`)
} else if (error instanceof NotFoundError) {
// 404 - Resource not found
console.log("Balance or plan not found")
} else if (error instanceof NetworkError) {
// Network issues
console.log(`Network error: ${error.message}`)
} else if (error instanceof CloseLoopError) {
// Other API errors
console.log(`API error: ${error.code} - ${error.message}`)
}
}Security
Input Validation
All inputs are validated using Zod schemas before being sent to the API:
- Wallet addresses: Must be valid Ethereum addresses (
0x+ 40 hex chars) - Product IDs: Non-empty strings with max 100 characters
- Credit amounts: Positive integers up to 1 billion
- Metadata: Sanitized to prevent prototype pollution attacks
Webhook Security
- HMAC-SHA256 signatures with timing-safe comparison
- Schema validation for all webhook payloads
- Type guards validate payload structure before use
TypeScript
All types are exported:
import type {
// Client
CloseLoopOptions,
// Credits
VerifyCreditsParams,
VerifyCreditsResponse,
ConsumeCreditsParams,
ConsumeCreditsResponse,
// Balances
CreditBalance,
CreditTransaction,
CreditStats,
GetBalanceParams,
ListBalancesParams,
ListBalancesResponse,
ListTransactionsParams,
ListTransactionsResponse,
// Webhooks
WebhookEvent,
WebhookEventType,
WebhookPayload,
PaymentSuccessPayload,
CreditsLowPayload,
CreditsExpiredPayload,
VerifyWebhookParams
} from "@closeloop/sdk"Configuration
const client = new CloseLoop({
// Required: Your API key from CloseLoop dashboard
apiKey: process.env.CLOSELOOP_API_KEY!,
// Optional: Custom API URL (for self-hosted instances)
baseUrl: "https://closeloop.app",
// Optional: Request timeout in milliseconds (default: 30000)
timeout: 30000
})Requirements
- Node.js 18.0.0 or higher
License
MIT
