Skip to content

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:

lib/wallet.ts
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 startup
const 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 spending
const wallet = createWallet({ storage });
// 4. Ledger + Checkout — top-ups via payment gateway
const 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 page

3. Handle webhooks

After payment, the gateway sends a webhook to your server. Set up an endpoint:

api/webhooks/midtrans.ts
import type { CheckoutManager, WebhookResult } from '@murai-wallet/murai';
// Example: Express / Hono / any framework
async 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):

lib/spend.ts
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 call
async 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:

lib/dashboard.ts
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?