Quick Start
This guide takes you from zero to a working token wallet in 5 minutes. You’ll set up storage, connect a payment gateway, and handle your first top-up.
1. Wire up the wallet
Create a lib/wallet.ts file in your project:
import { createCheckoutManager, createDrizzleStorage, createLedger, createMidtransGateway, createWallet,} from '@murai-wallet/murai';import { drizzle } from 'drizzle-orm/postgres-js';import postgres from 'postgres';
// 1. Storage — connect to PostgreSQL via Drizzle// biome-ignore lint/style/noNonNullAssertion: env vars validated at startupconst sql = postgres(process.env.DATABASE_URL!);const storage = createDrizzleStorage(drizzle(sql));
// 2. Gateway — Midtrans Snap (sandbox mode)const gateway = createMidtransGateway({ // biome-ignore lint/style/noNonNullAssertion: env vars validated at startup serverKey: process.env.MIDTRANS_SERVER_KEY!, // biome-ignore lint/style/noNonNullAssertion: env vars validated at startup clientKey: process.env.MIDTRANS_CLIENT_KEY!, sandbox: true,});
// 3. Wallet — balance queries and spendingconst wallet = createWallet({ storage });
// 4. Ledger + Checkout — top-ups via payment gatewayconst ledger = createLedger(storage);const checkout = createCheckoutManager(gateway, ledger, storage);
export { wallet, checkout };This gives you two objects:
wallet— balance queries and spending (getBalance,canSpend,spend)checkout— top-ups via payment gateway (createSession,handleWebhook)
2. Create a top-up session
When a user wants to buy tokens, create a checkout session and redirect them:
const session = await checkout.createSession({ userId: 'user_123', amount: 100_000, // IDR 100,000 successRedirectUrl: 'https://yourapp.com/success', failureRedirectUrl: 'https://yourapp.com/fail',});
// Redirect user to session.redirectUrl// They'll complete payment on the gateway's hosted page3. Handle webhooks
After payment, the gateway sends a webhook to your server. Set up an endpoint:
import type { CheckoutManager, WebhookResult } from '@murai-wallet/murai';
// Example: Express / Hono / any frameworkasync function handleMidtransWebhook( body: unknown, signatureKey: string, checkout: CheckoutManager,): Promise<{ status: number; body: WebhookResult | { error: string } }> { try { const result = await checkout.handleWebhook({ payload: body, signature: signatureKey, });
// result.action: 'credited' | 'skipped' | 'duplicate' return { status: 200, body: result }; } catch (error) { // WebhookVerificationError → 401 if (error instanceof Error && error.name === 'WebhookVerificationError') { return { status: 401, body: { error: 'Invalid signature' } }; } throw error; }}
export { handleMidtransWebhook };4. Spend tokens
Deduct tokens when your user consumes a service (e.g., an AI API call):
import { InsufficientBalanceError } from '@murai-wallet/murai';import type { Wallet } from '@murai-wallet/murai';
async function handleAIRequest(wallet: Wallet, userId: string, cost: number, requestId: string) { // 1. Check balance before calling the AI provider const canAfford = await wallet.canSpend(userId, cost); if (!canAfford) { return { error: 'Insufficient balance. Please top up your wallet.' }; }
// 2. Call your AI provider (OpenAI, Anthropic, etc.) const aiResponse = await callAIProvider(userId, requestId);
// 3. Deduct tokens — idempotency key prevents double-charges on retries try { await wallet.spend(userId, cost, `ai-${requestId}`); } catch (error) { if (error instanceof InsufficientBalanceError) { return { error: 'Balance changed. Please try again.' }; } throw error; }
return { result: aiResponse };}
// Placeholder for your AI provider callasync function callAIProvider(_userId: string, _requestId: string): Promise<string> { return 'AI response';}
export { handleAIRequest };The idempotency key (ai-${requestId}) prevents double-charges if your code retries.
5. Query history
Build a “my usage” dashboard with transaction and checkout history:
import type { Wallet } from '@murai-wallet/murai';
async function getUserDashboard(wallet: Wallet, userId: string) { // Current balance const balance = await wallet.getBalance(userId);
// Recent transactions (credits and debits) const recentTransactions = await wallet.getTransactions(userId, { limit: 20, });
// Only debits (spending history) const spendingHistory = await wallet.getTransactions(userId, { limit: 10, type: 'debit', });
// Checkout history (top-ups) const topUpHistory = await wallet.getCheckouts(userId, { status: 'paid', limit: 10, });
return { balance, recentTransactions, spendingHistory, topUpHistory };}
export { getUserDashboard };What’s next?
- Project Structure — understand the package layout
- Next.js Integration — full walkthrough with a working example app
- Webhook Verification — deep dive into security
- Architecture — understand the ledger, idempotency, and race condition handling
- Token Expiration — set expiry dates on credits and run expiration jobs
- Usage Reporting — generate usage reports and track spending over time