Implementing the wallet callback
This is usually the largest piece of work in the integration. Everything else — signing a launch call, embedding an iframe, listening for shell events — is a handful of lines. This is where you connect the platform to your actual player ledger.
moose-platform never holds player funds. Your wallet is the single source of truth, and the platform's job is to call two endpoints you implement, in real time, so every bet and win lands on it exactly once. See Wallet callback API reference for the field- level contract this guide walks through.
The two endpoints
// POST /v1/wallet/transaction — settle a BET, WIN, ROLLBACK, or ADJUSTMENT
// GET /v1/wallet/balance?playerRef=<ref> — answer a balance queryBoth are called by the platform, signed with your shared secret — verify every call before trusting it (see Signing & authentication). Register the base URL the platform sends them to through your tenant's portal — see Registering your wallet callback URL.
Handling each transaction type
async function handleTransaction(req: TransactionRequest): Promise<TransactionResponse> {
const cached = await ledger.getCachedResponse(req.transactionId)
if (cached) return cached // idempotent replay — see below, before anything else
let result: TransactionResponse
switch (req.type) {
case 'BET':
result = await ledger.debit(req.playerRef, req.amount, req.currency)
// result.status is 'DECLINED' here if the player doesn't have amount available —
// that's a normal business outcome, not an error
break
case 'WIN':
result = await ledger.credit(req.playerRef, req.amount, req.currency)
// never DECLINED — a real failure here should throw, not return DECLINED
break
case 'ROLLBACK':
const original = await ledger.getTransaction(req.originalTransactionId!)
result = await ledger.credit(req.playerRef, original.amount, req.currency)
break
case 'ADJUSTMENT':
result = req.direction === 'CREDIT'
? await ledger.credit(req.playerRef, req.amount, req.currency)
: await ledger.debit(req.playerRef, req.amount, req.currency)
break
}
await ledger.cacheResponse(req.transactionId, result)
return result
}BETis the only type whereDECLINEDis a valid response — insufficient funds, or outside your own bet limits. Everything else (WIN,ROLLBACK,ADJUSTMENT) must either succeed withOKor fail the HTTP call outright (non-200) — neverDECLINED.WINis a separate transaction from itsBET. Don't wait for aWINto reconcile a still-openBET— they arrive as independent calls, and whichever one carriesroundComplete: trueis the last for that round.ROLLBACKreverses a specificBETviaoriginalTransactionId— look up that bet's amount from your own ledger and credit it back. You'll also see a fallback rollback withsessionToken: "system:stale-round-reconciler"for a round the platform force-closed after the client disconnected — handle it the same way.ADJUSTMENTis rare and platform-admin-initiated.directiontells you which way to move the balance.
Idempotency is not optional
The platform retries a failed or timed-out call with the exact same transactionId. If your handler re-applies the balance change on a retry, a player gets double-charged or double-paid. Cache the response for every transactionId you've processed and return it verbatim on a repeat, before doing any ledger work — the pseudocode above checks this first for exactly that reason.
A safe pattern: make transactionId a unique constraint on your ledger table, and let a duplicate-key error on insert be your signal to look up and return the cached response instead of a second write.
Balance queries
async function handleBalance(playerRef: string): Promise<{ balance: number }> {
return { balance: await ledger.getBalance(playerRef) }
}Keep this fast and reliable — it's called live by game studios (via the platform) to show an up-to-date balance, not just as a background check. See Wallet callback API reference: GET /v1/wallet/balance for both callers.
Jackpot and other metadata
TransactionRequest.metadata is an opaque JSON object the platform stores and forwards to you verbatim — it never validates or interprets it. The common case is a jackpot WIN carrying pool/tier/amount detail:
if (req.metadata?.jackpot?.won) {
// display or log jackpot detail; your ledger's amount field is still
// the authoritative payout — metadata is supplementary, not a second
// source of truth for how much to credit
}See Data model & enums: Metadata & jackpot payouts for the exact (non-enforced) shape.
Respond within your timeout
The platform waits a bounded time for your response (5000ms by default — see Environments & base URLs) before treating the call as TIMED_OUT and retrying or reconciling later. As long as your idempotency handling is correct, there's no harm in a slow response occasionally triggering a retry — but a consistently slow wallet backend means more retries, more reconciliation traffic, and a worse player experience (the game client is waiting on the same round trip).
Next
- Free spins if you'll issue self-serve free-spin grants.
- Testing for verifying this end-to-end before real money is on the line.
- Go-live checklist before your first real player session.