@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/walletQuick 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
| Method | Endpoint | Returns |
|---|---|---|
getBalance(userId) | GET /balance | Promise<number> |
deduct(userId, feature, count?, idempotencyKey?) | POST /deduct | Promise<DeductResult> |
createTopupSession(input) | POST /topup-session | Promise<TopupSession> |
listTransactions(userId, opts?) | GET /transactions | Promise<TransactionsPage> |
getRates() | GET /rates | Promise<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 workDeductResult:
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 errorlistTransactions(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 type | Limit |
|---|---|
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:
getBalance→0on errordeduct→{ allowed: false }on errorcreateTopupSession→{ url: null }on errorlistTransactions→{ items: [], nextCursor: null }on error
This means a transient failure never silently grants usage or credits. See Errors & fail-closed for the full contract.