Integration/View as Markdown

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.

EndpointWhen we call itYou return
POST /authenticateThe game client loadsThe player, balance and client seed
POST /debitA spin, before the result is servedThe balance after the stake
POST /creditThe round endsThe payout and the new balance
POST /credit-rationalInstead of credit, for exact rational payouts (opt-in)The payout and the new balance
POST /rollbackA debit that never became a roundThe balance after the refund

Verify every request

Each request carries two headers:

  • x-munja-timestamp: milliseconds since the epoch
  • x-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 path
  • debited → 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": "…" }:

CodeMeaning
ERR_IPBInsufficient player balance
ERR_ISInvalid session token, or the session timed out
ERR_ATEAuthentication failed, or the token expired
ERR_GLEGambling limits exceeded
ERR_BNFBet not found
ERR_BEThe player already has an active bet
ERR_UEGeneral error; a rollback is expected
ERR_GEGeneral error; no rollback

The full request and response shapes are in the API reference.