Wallet API
Your server implements five endpoints under one base URL. We call them, signed, for every session and round. Balances never leave your platform.
| Endpoint | When we call it | You return |
|---|---|---|
POST /authenticate | The game client loads | The player, balance and client seed |
POST /debit | A spin, before the result is served | The balance after the stake |
POST /credit | The round ends | The payout and the new balance |
POST /credit-rational | Instead of credit, for exact rational payouts (opt-in) | The payout and the new balance |
POST /rollback | A debit that never became a round | The balance after the refund |
Verify every request
Each request carries two headers:
x-munja-timestamp: milliseconds since the epochx-munja-signature:hex(HMAC-SHA256(secret, "<timestamp>.<raw body>"))
The secret is your wallet signing secret, shown once when the wallet is configured. Verify over the raw body bytes, before parsing. Refuse a timestamp more than five minutes away from your clock.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(
secret: string,
timestamp: string,
rawBody: string,
signature: string,
) {
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60_000) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(signature, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
Amounts are micro-units
1,000,000 is one unit of the session currency: 2,500,000 USD is $2.50.
Amounts are integers. We never compute on them.
Settlement: debit, credit, rollback
A round is a small state machine on your side, keyed by roundId:
debited → credited: the normal pathdebited → rolled back: the failure path- nothing else: a credit after a rollback, or a rollback after a credit, is a refusal
debit takes amount and currency. Refuse with ERR_IPB when the
balance does not cover it.
credit sends no amount. It sends payoutMultiplier (hundredths of the
base stake) and costX100 (the mode's cost in hundredths). You compute:
payout = debited amount × payoutMultiplier / costX100 (floored)
The debited amount is the one you recorded under the same roundId. For a
bonus buy with cost 100× the debit is 100 base stakes, and this expression
pays the win on the base stake, as the game shows it.
rollback refunds the full debit. Answer ERR_BNF when you have no debit
for that roundId.
Idempotency
Every call of a round carries the same roundId. A repeated call must return
the result you already settled, never move money twice. A unique constraint
on (roundId, kind) in your ledger is the simplest way to guarantee it.
Refusals
Answer a refusal with a non-2xx status and { "code": "ERR_…", "message": "…" }:
| Code | Meaning |
|---|---|
ERR_IPB | Insufficient player balance |
ERR_IS | Invalid session token, or the session timed out |
ERR_ATE | Authentication failed, or the token expired |
ERR_GLE | Gambling limits exceeded |
ERR_BNF | Bet not found |
ERR_BE | The player already has an active bet |
ERR_UE | General error; a rollback is expected |
ERR_GE | General error; no rollback |
The full request and response shapes are in the API reference.