@anby/billing
v1.0.1
Published
Anby Billing SDK — debit, credit, balance queries against anby-billing-service. HMAC-signed service-to-service calls.
Maintainers
Readme
@anby/billing
Typed SDK for calling anby-billing-service from Anby services. Handles HMAC signing, request shape validation, idempotency derivation, and error mapping.
Install
npm add @anby/billing @anby/platform-sdk@anby/platform-sdk is a peer dependency — it owns the HMAC signer.
Bootstrap (once per service)
import { configureAuth } from '@anby/platform-sdk';
import { configureBilling } from '@anby/billing';
configureAuth({ hmacSecret: process.env.SVC_HMAC_SECRET });
configureBilling({
baseUrl: process.env.BILLING_SERVICE_URL, // https://billing.anby.ai
hmacSecret: process.env.SVC_HMAC_SECRET, // 32+ chars, shared with billing-service
serviceName: 'meeting', // source of debits
});Debit AC
import { debitCredits, InsufficientCreditsError } from '@anby/billing';
try {
const result = await debitCredits({
workspaceId: meeting.workspaceId,
amount: 5, // integer, whole AC
jobId: `meeting-summary:${meeting.id}`, // business identifier, unique per service
sourceService: 'meeting',
actorUserId: currentUser.id,
metadata: {
meetingId: meeting.id,
aiModel: 'gpt-5.4',
durationMinutes: 60,
},
});
console.log(`AC balance: ${result.totalBalanceAfter}`);
} catch (err) {
if (err instanceof InsufficientCreditsError) {
// show paywall / upsell CTA. err.required, err.available available
}
throw err;
}Calling debitCredits twice with the same (jobId, sourceService) returns the same ledgerEntryId both times — safe to retry on transient failures.
Get balance
const balance = await getBalance(workspaceId);
// { balanceSubscription, balanceTopup, total, cycle: { planSnapshot, periodEnd, ... } }Refund (AI failure / admin adjust)
import { refundCredits } from '@anby/billing';
await refundCredits({
workspaceId,
refundForJobId: `meeting-summary:${meeting.id}`,
reason: 'AI provider timeout',
sourceService: 'meeting',
});Errors
All errors extend BillingError with .code, .status, .details:
| Class | HTTP | When |
|-------|------|------|
| BillingConfigurationError | — | SDK not configured or invalid config |
| InvalidDebitRequestError | 400 | Zod validation fails locally or server rejects shape |
| InsufficientCreditsError | 402 | Balance below amount. .required, .available available |
| WorkspaceNotFoundError | 404 | No wallet for workspace |
| DuplicateRequestError | 409 | Same jobId already processed (server returns prior entry in .details) |
| WalletLockedError | 423 | Wallet frozen (admin action, fraud review) |
| BillingServiceUnavailableError | 5xx/network | Treat as retryable. SDK does not auto-retry |
Idempotency
jobIdis your business-meaningful identifier (e.g.meeting-summary:<meetingId>). One debit per(jobId, sourceService)globally.idempotencyKeyis the API-level key. SDK derives it assha256(jobId + sourceService)if you don't pass one. 24h TTL server-side.- Pass
idempotencyKeydirectly only if upstream (queue) already has a stable id. Most callers should only passjobId.
Design notes
- No auto-retry: caller decides. Billing is money-critical; silent retry could obscure balance bugs.
- Timeout default 10s: tunable via
timeoutMsinconfigureBilling. - Integer AC: fractional amounts rejected locally. See
docs/billing-system.mdfor the "round up to 1 AC" policy rationale.
