CreditManager — JavaScript
Orchestrates the full credit lifecycle: calculate → reserve → deduct.
Constructor
import { CreditManager, MemoryStore } from "@apoorwv/ducto";
const store = new MemoryStore();
const manager = new CreditManager(store);
Optionally pass a pre-configured PricingEngine and/or CreditEventEmitter:
import { CreditEventEmitter } from "@apoorwv/ducto";
const emitter = new CreditEventEmitter();
const manager = new CreditManager(store, engine, emitter);
Pricing
publishPricingFromDict(data)
manager.publishPricingFromDict({
version: 1,
models: { "_default": "input_tokens * (0.001 / 1000)" },
});
loadPricingFromStore()
Fetch active pricing from the database store.
publishPricing(config, label?)
Load from a PricingConfig object.
Balance Operations
addCredits(userId, amount, type?, metadata?, expiresAt?)
Add credits. Optional expiresAt for time-bound credits.
getBalance(userId)
reserveCredits(userId, amount, opType?, metadata?, minBalance?)
deduct(userId, metrics, idempotencyKey?, metadata?)
Full lifecycle: calculate cost → consume plan allowance → check spend caps → reserve → deduct.
deductFixed(userId, jobName, idempotencyKey?, metadata?)
Deduct a fixed cost (configured in pricing fixed section).
Plan Management
Plans provide free monthly allowances consumed before balance deductions.
store.setUserPlan(userId, planId)
Assign a plan to a user.
store.getUserPlan(userId)
Get user's current plan details.
store.checkAllowance(userId)
Get remaining free allowance for the current billing period.
Example — full plan coverage skips balance deduction:
manager.publishPricingFromDict({
version: 1,
models: { "_default": "input_tokens * 1" },
plans: { free: { id: "free", name: "Free", freeAllowance: 100 } },
});
await store.setUserPlan("user-1", "free");
await store.addCredits("user-1", 10);
const result = await manager.deduct("user-1", { inputTokens: 5 });
// result.amount === 0, balance unchanged — fully covered by allowance
Refunds
refundCredits(transactionId, amount?, reason?, metadata?)
Refund a previous deduction. Supports partial refunds. Returns error for duplicate refunds or unknown transactions.
const result = await manager.refundCredits(txId);
// result.amount === 1, result.newBalance === 100
Usage Analytics
Time-windowed aggregation queries. All accept start / end Date range.
| Method | Returns |
|---|---|
spendByUser(start, end) | SpendByUserRow[] — per-user totals |
spendByModel(start, end) | SpendByModelRow[] — per-model spend |
topUsers(limit, start, end) | TopUserRow[] — sorted by spend desc |
dailySpend(start, end) | DailySpendRow[] — bucketed by day |
aggregateStats(start, end) | AggregateStats — total, active users, avg daily, top model, top user |
Credit Expiry
sweepExpiredCredits(dryRun?)
Sweep expired credits from all users' balances. Dry-run reports without modifying.
await manager.addCredits("user-1", 100, "purchase", null, new Date("2024-01-01"));
const result = await manager.sweepExpiredCredits();
// result.expiredCount === 1, result.expiredAmount === 100
const dryRun = await manager.sweepExpiredCredits(true);
// dryRun === true, balance unchanged
Spend Caps
Per-user daily/monthly spend limits configured at the store level.
store.setSpendCap({
userId: "user-1",
type: "daily",
limit: 50,
action: "deny", // "deny" | "warn" | "notify"
model: "gpt-4", // optional per-model cap
});
- deny — throws
InsufficientCreditsErrorwhen exceeded - warn — emits
credits.cap_warningevent but allows the deduction - notify — emits
credits.cap_warningevent
Team / Shared Balances
Teams have their own credit pool separate from individual user balances.
| Method | Description |
|---|---|
store.createTeam(name, initialBalance?) | Create team with shared pool |
store.getTeamBalance(teamId) | Fetch team balance + member count |
store.addTeamMember(teamId, userId, role?, spendCap?) | Add member with optional per-user cap |
store.getTeamMembers(teamId) | List team members with spend + caps |
store.deductTeam(teamId, userId, cost, metadata?) | Deduct from team pool (user-attributed) |
manager.deductTeam(teamId, userId, metrics, metadata?) | Pricing-engine cost + team deduction |
const team = await store.createTeam("Engineering", 1000);
await store.addTeamMember(team.teamId, "user-1", "admin", { spendCap: 500 });
const result = await manager.deductTeam(team.teamId, "user-1", { inputTokens: 100 });
// result.amount === -100, result.teamBalanceAfter === 900
Events
Optional pub/sub event system for credit lifecycle events.
import { CreditEventEmitter } from "@apoorwv/ducto";
const emitter = new CreditEventEmitter();
const manager = new CreditManager(store, null, emitter);
emitter.on("credits.deducted", (event) => {
console.log(`${event.userId} spent ${event.data?.amount} credits`);
});
// Available events:
// "credits.deducted" | "credits.added" | "credits.refunded"
// | "credits.expired" | "credits.cap_reached"
// | "credits.cap_warning" | "credits.low_balance"
Properties
| Property | Type | Description |
|---|---|---|
pricingEngine | PricingEngine | null | Internal engine (null until loaded) |