Skip to content

About

Velobase Billing SDK for JavaScript/TypeScript

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@velobaseai/billing

Official Velobase Billing SDK for JavaScript and TypeScript.

Velobase is a customer-aware AI gateway: you route your app's LLM traffic through it, and every call is metered and billed against the end customer's credits wallet automatically. No token counting, no cost math, no settlement code on your side.

This SDK covers everything around that gateway:

  • Fund wallets — customers.deposit() when your customer pays you
  • Check balances — customers.get() for display and paywalls
  • Bill non-LLM work — billing.freeze/consume/unfreeze/deduct for media jobs, per-task charges, manual adjustments
  • Reconcile — usage.list() and receipts.get() over gateway-billed LLM traffic
  • Handle paywalls — parseGatewayInsufficientBalance() for typed 402 handling

The SDK does not wrap LLM calls. Your existing LLM SDK talks to the gateway directly; see Point LLM traffic at the Velobase gateway.

  • Zero runtime dependencies — uses native fetch
  • Works with Node.js 18+, Deno, Bun, and Cloudflare Workers
  • ESM and CommonJS dual build with full TypeScript declarations
  • Automatic retries with exponential backoff

Installation

npm install @velobaseai/billing
# or
pnpm add @velobaseai/billing
# or
yarn add @velobaseai/billing

Quick Start

import Velobase, { GATEWAY_HEADERS } from '@velobaseai/billing';
import { createOpenAI } from '@ai-sdk/openai';

const vb = new Velobase({ apiKey: 'vb_live_xxx' });

// 1. Your customer pays you — fund their wallet (creates the customer if new)
await vb.customers.deposit({
  customerId: 'user_123',
  amount: 5_000_000, // $5.00; 1 USD = 1,000,000 credits
});

// 2. Route LLM traffic through the gateway. Each call is metered and
//    billed against user_123's wallet automatically.
const openai = createOpenAI({
  baseURL: 'https://api.velobase.io/v1',
  apiKey: 'vb_live_xxx',
  headers: { [GATEWAY_HEADERS.customer]: 'user_123' },
});

// 3. Show the balance
const customer = await vb.customers.get('user_123');
console.log(customer.wallets.default.available); // credits remaining

How billing works

There are two billing paths, and the split is by workload:

LLM calls: implicit, at the gateway. Point your LLM SDK at the Velobase gateway with an X-Velobase-Customer header. The gateway freezes an estimate before forwarding upstream, settles the real token cost when the response finishes, and refunds on failure. You never call freeze or consume for LLM traffic; doing so would double-charge the customer.

Everything else: explicit, through this SDK. Media generation, per-task charges, manual adjustments — anything that is not an LLM call through the gateway — uses the ledger primitives directly:

deposit → freeze → consume   (normal flow)
                 → unfreeze  (failure/cancellation)
deposit → deduct             (immediate deduction)

Both paths drive the same per-customer ledger. Implicit gateway billing is the same freeze → consume/unfreeze cycle, executed by the gateway on your behalf; gateway-billed LLM calls and your explicit charges land in the same wallets and show up in the same ledger, usage, and receipt queries. The primitives are the foundation, not a legacy path.

  1. Deposit — Add raw credits to a customer's wallet. Creates the customer automatically on first deposit. 1 USD = 1,000,000 credits. Supports wallet/source for balance categories, and startsAt/expiresAt for time-limited credits.
  2. Freeze — Pre-authorize a credits amount before performing work. The frozen credits are deducted from available but not yet used. Each freeze is identified by a unique transactionId you provide.
  3. Consume — After the work is done, settle the frozen amount. You can pass actualAmount to charge less than what was frozen; the difference is automatically returned.
  4. Unfreeze — If the work fails or is cancelled, release the full frozen amount back to the customer.
  5. Deduct — Directly deduct credits from a customer without freezing first. Useful for immediate charges.
  6. Ledger — Query a customer's transaction history with filtering and cursor-based pagination.
  7. Gateway usage / receipts — Query LLM gateway usage rows and per-transaction receipts for reconciliation.

All write operations are idempotent — repeating the same transactionId (freeze/consume/unfreeze/deduct) or idempotencyKey (deposit) returns the original result without double-charging. Gateway LLM requests accept an Idempotency-Key header with the same guarantee.

Configuration

const vb = new Velobase({
  apiKey: 'vb_live_xxx',             // Required. Your Velobase API key.
  baseUrl: 'https://api.velobase.io', // Optional. Override the API endpoint.
  timeout: 30000,                     // Optional. Request timeout in ms (default: 30s).
  maxRetries: 2,                      // Optional. Retry count on 5xx/network errors (default: 2).
});

When baseUrl is omitted, the SDK resolves the endpoint as: explicit baseUrl option > VELOBASE_BASE_URL environment variable > https://api.velobase.io. SDK defaults are always production-safe; published releases never bake in a non-production URL.

Environments

  • Production — omit baseUrl (or pin https://api.velobase.io). Use the stable npm release.
  • Development / staging — set VELOBASE_BASE_URL in the deployment's environment (or pass baseUrl explicitly), and keep its API key separate from production. A staging deployment must never point at the production ledger: it would bill real customer wallets. Guard for this at startup rather than relying on configuration discipline.
  • The gateway (LLM traffic) and the control plane (this SDK) accept the same project API key and, in standard deployments, the same host. One key, one host, both planes.

Usage Examples

Point LLM traffic at the Velobase gateway

The billing SDK does not wrap LLM calls. Use your LLM SDK with the Velobase gateway base URL, pass your project API key as the upstream API key, and attach the customer/trace headers for implicit per-call billing.

import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import Velobase, { GATEWAY_HEADERS } from '@velobaseai/billing';

const gatewayUrl = 'https://api.velobase.io';
const gatewayApiKey = process.env.VELOBASE_GATEWAY_API_KEY!;
const customerId = 'user_123';
const traceId = 'turn_abc';

const anthropic = createAnthropic({
  baseURL: `${gatewayUrl}/v1`,
  apiKey: gatewayApiKey,
  headers: {
    [GATEWAY_HEADERS.customer]: customerId,
    [GATEWAY_HEADERS.traceId]: traceId,
  },
});

const openai = createOpenAI({
  baseURL: `${gatewayUrl}/v1`,
  apiKey: gatewayApiKey,
  headers: {
    [GATEWAY_HEADERS.customer]: customerId,
    [GATEWAY_HEADERS.traceId]: traceId,
  },
});

const vb = new Velobase({ apiKey: gatewayApiKey, baseUrl: gatewayUrl });
const usage = await vb.usage.list({ traceId });

Billing at the gateway is implicit: an estimate is frozen before the request goes upstream, the real token cost settles when the response completes, and failed requests are refunded. Do not freeze or consume for these calls.

For retried LLM requests, send an Idempotency-Key header per logical request: a replayed request returns a 409 instead of charging twice.

Handle insufficient balance (402 paywall)

When a customer's wallet cannot cover the estimated cost, the gateway rejects the call with HTTP 402 before any tokens are spent. parseGatewayInsufficientBalance() extracts the typed paywall payload from whatever error shape your LLM SDK throws:

import { parseGatewayInsufficientBalance } from '@velobaseai/billing';

try {
  await streamText({ model, messages });
} catch (err) {
  const paywall = parseGatewayInsufficientBalance(err);
  if (paywall) {
    // Show your top-up UI. All amounts are credits (1 USD = 1,000,000 credits).
    console.log(paywall.requiredCredits);   // estimated cost of the rejected call
    console.log(paywall.availableCredits);  // what the customer has left
    console.log(paywall.requiredUsd);       // same, pre-formatted in USD
  } else {
    throw err;
  }
}

Tip: requiredCredits reflects the gateway's pre-call hold. Its output part is sized from the request's max_tokens; a request that omits max_tokens reserves for a large 8,000-token output and gets a much higher paywall threshold than the call will actually cost. Always set max_tokens on gateway calls. Image and PDF inputs are supported on gateway chat calls, and their hold is estimated from image dimensions and PDF page count, so media requests keep sane thresholds too.

Deposit with idempotency

// Safe to retry — the second call returns the same result without double-charging
const result = await vb.customers.deposit({
  customerId: 'user_123',
  amount: 5_000_000, // $5.00; 1 USD = 1,000,000 credits
  idempotencyKey: 'order_abc_payment',
  description: 'Purchase, $5.00',
});

console.log(result.addedAmount);        // 5000000
console.log(result.isIdempotentReplay); // false on first call, true on retries

Deposit with wallet and expiry

const result = await vb.customers.deposit({
  customerId: 'user_123',
  amount: 10_000_000, // $10.00; 1 USD = 1,000,000 credits
  wallet: 'bonus',
  source: 'annual_campaign',
  startsAt: '2025-01-01T00:00:00Z',
  expiresAt: '2025-12-31T23:59:59Z',
  description: 'Annual bonus, $10.00',
});
console.log(result.wallet); // 'bonus'
console.log(result.source); // 'annual_campaign'
console.log(result.startsAt);  // '2025-01-01T00:00:00.000Z'
console.log(result.expiresAt); // '2025-12-31T23:59:59.000Z'

Deposit with customer metadata

const result = await vb.customers.deposit({
  customerId: 'user_123',
  amount: 10_000_000,
  name: 'Alice',
  email: 'alice@example.com',
  metadata: { plan: 'pro', source: 'stripe' },
});

Bill non-LLM work (freeze-then-consume)

Use the ledger primitives for work that does not go through the gateway: media generation, long-running jobs, one-off charges.

const CUSTOMER = 'user_123';

// Generate transactionId once and persist before freezing
const transactionId = `${CUSTOMER}_${crypto.randomUUID().replace(/-/g, '')}`;

// Check balance before starting
const before = await vb.customers.get(CUSTOMER);
console.log('Available:', before.wallets.default.available);

// Freeze the estimated cost
await vb.billing.freeze({
  customerId: CUSTOMER,
  amount: 100,
  transactionId,
  businessType: 'TASK',
  description: '1080p video, ~60s',
});

// ... do the work ...

// Settle with the actual cost (partial consumption)
const result = await vb.billing.consume({ transactionId, actualAmount: 73 });
console.log('Charged:', result.consumedAmount);  // 73
console.log('Returned:', result.returnedAmount); // 27

// Verify final balance
const after = await vb.customers.get(CUSTOMER);
console.log('Available:', after.wallets.default.available);

Direct deduct (without freezing)

const CUSTOMER = 'user_123';
const transactionId = 'api_call_001';

const result = await vb.billing.deduct({
  customerId: CUSTOMER,
  amount: 5,
  transactionId,
  businessType: 'TASK',
  description: 'API call charge',
});
console.log('Deducted:', result.deductedAmount); // 5
console.log('At:', result.deductedAt);

Query gateway usage and receipts

const usage = await vb.usage.list({
  customerId: 'user_123',
  traceId: 'turn_abc',
});

for (const row of usage.data) {
  console.log(row.transactionId, row.customerCostCredits, row.status);
}

const receipt = await vb.receipts.get(usage.data[0].transactionId);
console.log(receipt.cost.credits, receipt.balance.credits);

Query transaction ledger

// List all ledger entries (default limit=20)
const ledger = await vb.customers.ledger('user_123');
for (const entry of ledger.items) {
  console.log(entry.operationType, entry.amount, entry.wallet, entry.source, entry.createdAt);
}
console.log('Total:', ledger.totalCount);

// Filter by operation type
const grants = await vb.customers.ledger('user_123', { operationType: 'GRANT' });

// Filter by transactionId
const txnEntries = await vb.customers.ledger('user_123', { transactionId: 'job_abc' });

// Paginate with cursor
const page1 = await vb.customers.ledger('user_123', { limit: 10 });
if (page1.hasMore) {
  const page2 = await vb.customers.ledger('user_123', { limit: 10, cursor: page1.nextCursor });
}

Customer balance structure

const customer = await vb.customers.get('user_123');

// Wallets are keyed by wallet/category name.
const defaultWallet = customer.wallets.default;
defaultWallet.total;     // total deposited
defaultWallet.used;      // total consumed
defaultWallet.frozen;    // currently frozen (pending)
defaultWallet.available; // total - used - frozen

// Sources inside each wallet preserve validity windows.
for (const source of defaultWallet.sources) {
  console.log(source.source);      // 'default', 'stripe_checkout', etc.
  console.log(source.available);
  console.log(source.startsAt);    // null or ISO date string
  console.log(source.expiresAt);   // null or ISO date string
}

API Reference

vb.customers.deposit(params): Promise<DepositResponse>

Deposit funds. Creates the customer if they don't exist.

Parameter Type Required Description
customerId string Yes Your unique customer identifier
amount number Yes Raw credits to deposit. Must be a positive integer. 1 USD = 1,000,000 credits.
wallet string No Wallet/category to credit. Defaults to "default" on server.
source string No Optional source label for this grant. Defaults to "default" on server.
startsAt string No ISO datetime string. When the credits become active.
expiresAt string No ISO datetime string. When the credits expire. Must be after startsAt.
idempotencyKey string No Prevents duplicate deposits on retry
name string | null No Customer display name
email string | null No Customer email
metadata object No Arbitrary key-value metadata
description string No Description for the deposit

Returns: { customerId, accountId, wallet, source, totalAmount, addedAmount, startsAt, expiresAt, recordId, isIdempotentReplay }

vb.customers.get(customerId): Promise<CustomerResponse>

Retrieve a customer's balance and account details.

Returns: { id, name, email, metadata, balance, accounts, createdAt }

vb.customers.ledger(customerId, params?): Promise<LedgerResponse>

Query a customer's transaction history with filtering and cursor-based pagination.

Parameter Type Required Description
customerId string Yes Customer identifier (positional)
params.limit number No Page size (1–100, default 20)
params.cursor string No Cursor from a previous nextCursor for pagination
params.operationType string No Filter by operation type: FREEZE, CONSUME, UNFREEZE, GRANT, EXPIRE
params.transactionId string No Filter by transaction ID

Returns: { items: LedgerEntry[], totalCount, hasMore, nextCursor }

Each LedgerEntry has: id, operationType, amount, wallet, source, transactionId, businessType, description, accountId, status, createdAt

vb.billing.freeze(params): Promise<FreezeResponse>

Freeze credits before performing work.

Parameter Type Required Description
customerId string Yes Customer identifier
amount number Yes Credits to freeze (must be > 0). 1 credit = 1 micro-USD.
transactionId string Yes Your unique ID for this operation (idempotency key)
businessType BusinessType No Business category. See businessType for accepted values.
description string No Human-readable description

Returns: { transactionId, frozenAmount, freezeDetails, isIdempotentReplay }

vb.billing.consume(params): Promise<ConsumeResponse>

Settle a frozen amount. Supports partial consumption.

Parameter Type Required Description
transactionId string Yes The transactionId from the freeze
actualAmount number No Actual amount to charge. Defaults to full frozen amount.

Returns: { transactionId, consumedAmount, returnedAmount, consumeDetails, consumedAt, isIdempotentReplay }

vb.billing.unfreeze(params): Promise<UnfreezeResponse>

Release a frozen amount back to the customer.

Parameter Type Required Description
transactionId string Yes The transactionId from the freeze

Returns: { transactionId, unfrozenAmount, unfreezeDetails, unfrozenAt, isIdempotentReplay }

vb.billing.deduct(params): Promise<DeductResponse>

Directly deduct credits without freezing first. Useful for immediate charges.

Parameter Type Required Description
customerId string Yes Customer identifier
amount number Yes Credits to deduct (must be > 0). 1 credit = 1 micro-USD.
transactionId string Yes Your unique ID for this operation (idempotency key)
businessType BusinessType No Business category. See businessType for accepted values.
description string No Human-readable description

Returns: { transactionId, deductedAmount, deductDetails, deductedAt, isIdempotentReplay }

vb.usage.list(params?): Promise<UsageListResponse>

List LLM gateway usage rows for the current project. This endpoint is intended for reconciliation and trace-level reporting after LLM traffic has gone through the Velobase gateway.

Parameter Type Required Description
customerId string No Filter by X-Velobase-Customer
model string No Filter by public gateway model id
transactionId string No Filter by gateway transaction id
traceId string No Filter by X-Velobase-Trace-Id
cursor string No Cursor from a previous nextCursor
limit number No Page size

Returns: { object: "list", data: UsageListItem[], hasMore, nextCursor }

vb.receipts.get(transactionId): Promise<ReceiptResponse>

Retrieve one LLM gateway billing receipt by transaction id.

Returns: { object: "billing.receipt", transactionId, status, customerId, traceId, model, usage, cost, frozen, balance, upstream, created }

GATEWAY_HEADERS

Header-name constants for pointing LLM traffic at the gateway:

Constant Header Purpose
GATEWAY_HEADERS.customer X-Velobase-Customer Which end customer to bill. Required on billable gateway calls made with a project key.
GATEWAY_HEADERS.traceId X-Velobase-Trace-Id Optional correlation id. Query it back with usage.list({ traceId }).

parseGatewayInsufficientBalance(error): GatewayPaywall | null

Extract the typed 402 paywall payload from any error thrown by an LLM SDK whose request was rejected by the gateway for insufficient balance. Returns null when the error is anything else.

Returns: { requiredCredits, availableCredits, estimatedCredits, requiredUsd, availableUsd }

transactionId

transactionId uniquely identifies one billing operation (freeze → consume/unfreeze cycle, or a single deduct) and acts as its idempotency key. The server uses it to prevent double-charging on retries.

Recommended format: {customerId}_{uuid}

// Generate once per billing operation, then persist it
const transactionId = `${customerId}_${crypto.randomUUID().replace(/-/g, '')}`;
// e.g. "user_123_a3f8c21d4e0b4a9f8c1d2e3f4a5b6c7d"

Rules:

  • Generate once and store — create the ID before calling freeze() or deduct(), save it to your database, and reuse the same value on retries
  • Never regenerate at the call site — calling crypto.randomUUID() inside freeze() produces a different ID on every attempt, breaking idempotency
  • Unique within your project — two different billing operations must not share the same transactionId
// Wrong — new UUID on every call, idempotency broken on retry
await vb.billing.freeze({
  customerId,
  amount: 50,
  transactionId: `${customerId}_${crypto.randomUUID()}`, // ❌ regenerated each time
});

// Correct — UUID generated once and persisted before calling freeze
const transactionId = await db.getOrCreateTransactionId(operationId, customerId);
// e.g. returns existing ID or stores `${customerId}_${crypto.randomUUID()}` on first call

await vb.billing.freeze({ customerId, amount: 50, transactionId });
// Safe to retry — same transactionId returns the original result
await vb.billing.freeze({ customerId, amount: 50, transactionId });

businessType

businessType is an optional field on freeze() and deduct() that categorises the billing operation for analytics and reconciliation. The SDK validates the value client-side before sending the request.

Accepted values:

Value Description
UNDEFINED Default / unclassified (server default when omitted)
TASK Async task execution (e.g. video generation, image processing)
ORDER One-time purchase or order fulfilment
MEMBERSHIP Membership plan credit grant
SUBSCRIPTION Subscription renewal credit grant
FREE_TRIAL Free-trial credit grant
ADMIN_GRANT Manually granted credits by an admin

Passing an unrecognised value throws an Error immediately — before any network request is made.

import Velobase, { type BusinessType } from '@velobaseai/billing';

const vb = new Velobase({ apiKey: 'vb_live_xxx' });

await vb.billing.freeze({
  customerId: 'user_123',
  amount: 50,
  transactionId: 'job_abc',
  businessType: 'TASK',        // ✅ IDE autocomplete + client-side validation
});

await vb.billing.freeze({
  customerId: 'user_123',
  amount: 50,
  transactionId: 'job_abc',
  businessType: 'INVALID_VAL', // ❌ throws Error before making a network call
});

Error Handling

All API errors throw typed exceptions that extend VelobaseError:

import {
  VelobaseError,
  VelobaseAuthenticationError,
  VelobaseValidationError,
  VelobaseNotFoundError,
} from '@velobaseai/billing';

try {
  await vb.billing.freeze({
    customerId: 'user_123',
    amount: 999999,
    transactionId: 'job_xyz',
  });
} catch (err) {
  if (err instanceof VelobaseValidationError) {
    // 400 — bad request or insufficient balance
    console.error(err.message); // "insufficient balance"
  } else if (err instanceof VelobaseAuthenticationError) {
    // 401 — invalid or missing API key
  } else if (err instanceof VelobaseNotFoundError) {
    // 404 — customer not found
  } else if (err instanceof VelobaseError) {
    // catch-all for other API errors
    console.error(err.status, err.type, err.message);
  }
}
Error Class HTTP Status When
VelobaseAuthenticationError 401 Invalid or missing API key
VelobaseValidationError 400 Bad params, insufficient balance
VelobaseNotFoundError 404 Customer or resource not found
VelobaseConflictError 409 Conflicting operation
VelobaseInternalError 500 Server-side error (auto-retried)

Retries

The SDK automatically retries on 5xx errors and network failures with exponential backoff (500ms, 1s, 2s..., capped at 5s). Retries are safe because all Velobase write operations are idempotent.

4xx errors (validation, auth, not found) are never retried.

CommonJS

const { Velobase } = require('@velobaseai/billing');

const vb = new Velobase({ apiKey: 'vb_live_xxx' });

Versioning and release channels

  • latest — stable releases. Pin these in production (npm install @velobaseai/billing).
  • dev — prerelease builds (x.y.z-dev.N) for development branches (npm install @velobaseai/billing@dev). Never ship a -dev build to production.

SDK defaults are always production-safe: the default baseUrl points at the production API in every release, on both channels.

License

MIT

About

Velobase Billing SDK for JavaScript/TypeScript

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages