MisarMisar Docs
MisarMailMisar.BlogMisarReachMisarPostMisar.DevMisarSEOMisar PlatformMisar SSO
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[]>
getTopupDiscounts()GET /ratesPromise<TopupDiscounts>

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, totalCredits, bonusCredits, tierLabel } = await wallet.createTopupSession({
  userId: "usr_123",
  amountDollars: 250,             // integer 10–100000
  product: "blog",
  returnUrl: "https://www.misar.blog/dashboard/billing",
});

// Preview the grant before checkout: e.g. 265 credits (250 + 15 bonus, "6% bonus credits").
console.log(`You'll get ${totalCredits} credits (${bonusCredits} bonus — ${tierLabel})`);

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

The preview fields (baseCredits, bonusCredits, totalCredits, bonusPercent, tierLabel) are informational — the grant is finalized server-side by the webhook.

getTopupDiscounts()

const discounts = await wallet.getTopupDiscounts();
// → { model: "bonus_credits", minTopupDollars: 10, maxTopupDollars: 100000,
//     tiers: [{ minDollars: 0, bonusPercent: 0, label: "No bonus" }, …] }

Read the volume-discount tiers instead of hardcoding bonus labels in product code — a server-side tier change then applies everywhere at once.

computeTopupCredits(amountDollars, tiers?)

A pure helper (no network call) that mirrors the server's calculation, for rendering a credit preview client-side:

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

const { baseCredits, bonusCredits, totalCredits, tierLabel } = computeTopupCredits(250);
// → { baseCredits: 250, bonusCredits: 15, totalCredits: 265, bonusPercent: 6, tierLabel: "6% bonus credits" }

Pass the live tiers from getTopupDiscounts() as the second argument to stay perfectly in sync with the server; omit it to use the mirrored defaults.

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.