Misar Docs
MisarMailMisar.BlogMisarReachMisarPostMisar.DevMisar PlatformMisar IdentityMisar Posts API
Wallet

@misar/wallet SDK

Typed TypeScript client for the Universal Wallet: getBalance, deduct, createTopupSession, listTransactions, getRates.

@misar/wallet is the typed client for the Universal Wallet REST API. It wraps every endpoint, applies the service-key header, and enforces the fail-closed contract so a ledger outage can never grant free usage.

Installation

npm install @misar/wallet
# or
pnpm add @misar/wallet

Quick start

import { createWalletClient } from "@misar/wallet";

export const wallet = createWalletClient({
  baseUrl: process.env.WALLET_API_URL,          // default: https://api.misar.io/io/wallet
  serviceKey: process.env.WALLET_SERVICE_KEY!,  // server-only secret
});

Server-side only

The client holds WALLET_SERVICE_KEY. Instantiate it in server code (route handlers, server actions, workers) only — never in a client component or browser bundle.

createWalletClient(options)

Prop

Type

Methods

MethodEndpointReturns
getBalance(userId)GET /balancePromise<number>
deduct(userId, feature, count?, idempotencyKey?)POST /deductPromise<DeductResult>
createTopupSession(input)POST /topup-sessionPromise<TopupSession>
listTransactions(userId, opts?)GET /transactionsPromise<TransactionsPage>
getRates()GET /ratesPromise<CreditRate[]>

getBalance(userId)

const credits = await wallet.getBalance("usr_123");
// → 42  (1 credit = $1). Returns 0 on any error so the UI degrades gracefully.

deduct(userId, feature, count?, idempotencyKey?)

const result = await wallet.deduct(
  "usr_123",
  "blog.article.generate",
  1,                          // count (optional, default 1)
  "article:my-post-slug",     // idempotency_key (optional, makes retries safe)
);

if (!result.allowed) {
  // Insufficient credits or the ledger was unreachable (fail-closed).
  // result.required tells you the shortfall.
  throw new Error("Out of credits");
}
// proceed with the billable work

DeductResult:

Prop

Type

createTopupSession(params)

const { url } = await wallet.createTopupSession({
  userId: "usr_123",
  amountDollars: 25,              // integer 10–100000
  product: "blog",
  returnUrl: "https://www.misar.blog/dashboard/billing",
});

if (url) redirect(url); // url is null on error

listTransactions(userId, opts?)

let cursor: string | undefined;
do {
  const { items, nextCursor } = await wallet.listTransactions("usr_123", { limit: 50, cursor });
  // process items…
  cursor = nextCursor ?? undefined;
} while (cursor);

TransactionsPage is { items: WalletTransaction[]; nextCursor?: string }.

getRates()

const rates = await wallet.getRates();
// → [{ feature: "blog.article.generate", label: "Generate an article", credits: 1, unit: "article" }, …]

Rate limits

Endpoint typeLimit
Write (deduct, topup-session)50 ops / user / 60 s
Read (balance, transactions)200 ops / user / 60 s

Exceeding the limit returns 429. The Retry-After header tells you how many seconds to wait. The SDK throws a WalletError with status: 429 — catch it and back off before retrying.

import { WalletError } from "@misar/wallet";

try {
  const result = await wallet.deduct(userId, "email_send", 1);
} catch (err) {
  if (err instanceof WalletError && err.status === 429) {
    // back off and retry after the header value
  }
}

Error handling

The SDK mirrors the API's fail-closed contract rather than throwing on network errors:

  • getBalance0 on error
  • deduct{ allowed: false } on error
  • createTopupSession{ url: null } on error
  • listTransactions{ items: [], nextCursor: null } on error

This means a transient failure never silently grants usage or credits. See Errors & fail-closed for the full contract.