Seamless wallet

TALOS does not store the player balance. On every balance query and every transaction it sends a callback to your walletUrl, and your reply is the source of truth. The full specification is in Wallet callback.

Request#

POST to your walletUrl, with Content-Type: application/json and the signed headers (see Validating calls from TALOS).

FieldPresent inDescription
actionallbalance, bet, win, refund or rollback.
player_idallThe player_id you sent at launch.
currencyallMerchant currency.
providerallGame provider code (SPORTSBOOK for the sportsbook).
gameallGame code.
tx_idtransactionsUnique transaction id. Idempotency key.
round_idtransactionsRound (or bet, in the sportsbook) the transaction belongs to.
amounttransactionsAmount, always positive, in the major currency unit (e.g. 2.5 = US$ 2.50; up to 2 decimals; CLP has none).
ref_tx_idrefund, rollbacktx_id of the reverted transaction.

Treat tx_id, round_id and ref_tx_id as opaque strings.

Actions#

ActionBalance effectWhat to do
balancenoneReturn the current balance.
betdebit of amountDebit. Without funds, reject with INSUFFICIENT_FUNDS.
wincredit of amountCredit. A round without a win generates no win.
refundcreditReturn the ref_tx_id bet (same amount). If you never applied that bet, reply TRANSACTION_NOT_FOUND.
rollbackdebitUndo the ref_tx_id win (same amount). If you never applied that win, reply TRANSACTION_NOT_FOUND.

The normal order of a round is bet → win. Some providers send the win before the bet; do not rely on the order, rely on the tx_id.

Response#

Always reply with JSON:

JSON
{ "ok": true, "balance": 97.5 }
JSON
{ "ok": false, "error": "INSUFFICIENT_FUNDS", "balance": 1.2 }
  • balance is the balance after the transaction, in the major currency unit. It is required when ok is true.
  • In rejections, balance is optional (it helps the provider show the right balance).
  • The error code is read in uppercase. Use these when they apply:
errorWhen
INSUFFICIENT_FUNDSNot enough balance for the bet.
PLAYER_NOT_FOUNDUnknown player_id.
PLAYER_BLOCKEDPlayer blocked (self-exclusion, fraud, limit).
TRANSACTION_NOT_FOUNDIn refund/rollback: you never applied the ref_tx_id transaction.

Any other code is also accepted and treated as a rejection.

Idempotency#

The same tx_id can arrive more than once — resends after a timeout, retries from the provider or the sportsbook. For each tx_id:

  1. The first time, apply it and store the result (success with the balance, or the rejection).
  2. The following times, do not apply it again: return the same result (with the current balance, if you prefer).

Do the check and the debit in one database transaction (e.g. tx_id with a unique index), so two simultaneous requests with the same tx_id never debit twice.

Never apply the same tx_id twice

TALOS reuses the same tx_id when resending the same transaction precisely so you can deduplicate. If your endpoint is not idempotent, a timeout followed by a resend debits the player twice.

Unknown result and resends#

TALOS considers the reply unavailable — the result on your side is unknown — when:

  • there is no reply within 8 seconds (default timeout);
  • the connection fails;
  • the HTTP status is 5xx;
  • the body is not JSON, has no boolean ok, or has ok: true without a numeric balance.

In those cases the transaction stays pending. It is resolved by a resend with the same tx_id (when the provider or the sportsbook repeats the operation) or by a refund that references it. So, if you received and applied the bet but the reply did not arrive, the resend must return the same success; and if you never received the bet, its refund must reply TRANSACTION_NOT_FOUND.

Replies with an HTTP status below 500 are read from the body: a 401 or 400 with { "ok": false, "error": "..." } is a rejection, not an outage.

Final rejections#

  • A rejected bet (bet) or rollback is final: a resend of the same tx_id returns the same rejection without calling you again.
  • A rejected credit (win, refund) is resent when the provider repeats the operation. Avoid rejecting credits for business rules — the money belongs to the player.
  • The same tx_id with a different amount is rejected by TALOS (TRANSACTION_MISMATCH) without calling your wallet.

Sportsbook#

Sportsbook bets use the same wallet, with provider: "SPORTSBOOK" and game: "sportsbook". The round_id is the bet id.

  • bet — one per bet in the betslip, when the player places it. If one bet of the betslip is rejected, the bets of the same betslip that were already debited get a refund; if the reply to a bet never arrives (unknown result), it also gets a refund — reply TRANSACTION_NOT_FOUND if you never applied it.
  • win — on settlement with an amount to pay (won bet, partial return or voided bet). A lost bet generates no win.
  • refund — the reversal described above, with ref_tx_id = the bet tx_id.

The sportsbook does not send rollback today. refund and win without a final reply are resent with increasing backoff (1 s, 2 s, 4 s… up to 5 minutes between attempts), always with the same tx_id.

Endpoint example#

// app/api/talos/wallet/route.ts (Next.js) — sketch; adapt it to your database
import { verifyTalos } from './verify' // see Authentication

export async function POST(request: Request) {
  const raw = await request.text()
  if (!verifyTalos(request, raw)) {
    return Response.json({ ok: false, error: 'INVALID_SIGNATURE' }, { status: 401 })
  }
  const cb = JSON.parse(raw)

  if (cb.action === 'balance') {
    const balance = await wallet.balanceOf(cb.player_id, cb.currency)
    return Response.json({ ok: true, balance })
  }

  // idempotency: the same tx_id returns the stored result
  const previous = await wallet.findMove(cb.tx_id)
  if (previous) return Response.json(previous.reply)

  const reply = await wallet.apply(cb) // debit/credit + store tx_id and reply in the same transaction
  return Response.json(reply)
}

Reconciliation#

Every transaction sent to your wallet, with its status (success, pending, failed) and the returned error, is in GET /v1/transactions. Use it to reconcile with your ledger.