The wallet aggregator facade exposes a versioned inbound wallet endpoint for casino game aggregators:
POST /api/wallet/v1/providers/{slug}/callback
Providers are configured in Admin -> Finance -> Wallet Aggregator. Each provider selects one adapter (mock, softswiss, pragmatic, or evoplay), a wallet currency (GOLD or DIAMOND), rollback window, rate limit, optional IP allowlist, and encrypted adapter config.
Canonical Operations
All adapters normalize callbacks to the same internal contract.
| Operation | Effect |
|---|---|
balance | Resolves the wallet session and returns the current wallet balance. No mutation. |
debit | Debits a bet from the provider currency wallet, writes a core Transaction with type AGGREGATOR_BET, writes WalletTransaction, and opens/appends a GameRound. |
credit | Credits a win, writes AGGREGATOR_WIN, updates round credit totals, and optionally settles the round when the adapter payload sets roundFinished. |
rollback | Reverses a prior debit or credit by referenceTxId, writes AGGREGATOR_ROLLBACK, and marks the original wallet transaction as rolled back. |
Amounts are decimal strings with at most two fractional digits. Responses serialize balances as strings.
Idempotency And Replay
providerTxIdis the idempotency anchor.WalletTransactionhas a unique(providerId, providerTxId)constraint.- Core ledger rows use
paymentMethod = wallet_aggregatorandidempotencyKey = wallet:{providerId}:{providerTxId}. - Duplicate callbacks with the same operation, amount, and reference return the persisted response with
duplicate: true. - Duplicate callbacks with a different amount, operation, or reference are rejected with
DUPLICATE_TX_MISMATCHand create aTX_MISMATCHdiscrepancy. - Redis replay reservations use
wallet:replay:{providerId}:{providerTxId}with a TTL at least as long as the rollback window. - When Redis is configured and unavailable, callback guards fail closed with
REPLAY_REJECTED.
Sessions
Callbacks never create users. A wallet session maps a provider token or external user ID to an existing platform User.
Admin/dev session creation:
POST /api/v1/admin/wallet-aggregator/sessions
Content-Type: application/json
{
"providerId": "provider-uuid",
"userId": "user-uuid",
"ttlSeconds": 3600,
"externalUserId": "optional-provider-player-id"
}
The response returns the plaintext token once. Only its SHA-256 hash is stored.
Rollbacks
- Rollbacks require
referenceTxId. - Repeated rollback of the same reference returns the original rollback result.
- Rollback-before-debit creates a zero-amount tombstone. If the late debit later arrives, it is rejected.
- Expired rollback windows return
ROLLBACK_WINDOW_EXPIRED. - Settled debit rounds reject rollback by default with
ROUND_FINALIZED. - Credit rollback clamps at zero if the user lacks the credited amount and creates a
CLAMPED_ROLLBACKdiscrepancy.
Error Vocabulary
Canonical errors are mapped by each adapter:
| Canonical Code | Meaning |
|---|---|
INSUFFICIENT_FUNDS | Wallet balance cannot cover a debit. |
USER_NOT_FOUND | Session token or mapped user is missing/expired. |
USER_BLOCKED | User is blocked or frozen. |
TX_NOT_FOUND | Referenced round or transaction is missing. |
ROUND_FINALIZED | Round cannot accept the requested mutation. |
ROLLBACK_WINDOW_EXPIRED | Rollback is outside provider window. |
DUPLICATE_TX_MISMATCH | Provider transaction ID was reused with different payload. |
INVALID_SIGNATURE | Adapter signature check failed. |
REPLAY_REJECTED | Replay/rate guard rejected or guard storage was unavailable. |
PROVIDER_DISABLED | Provider is missing, inactive, disallowed by IP, or adapter is unavailable. |
INVALID_REQUEST | Payload is malformed or violates the canonical contract. |
INTERNAL_ERROR | Temporary internal failure. Aggregators should retry. |
Adapter Examples
Mock
Config:
{ "secret": "test-secret" }
Signature: X-Wallet-Signature = hex(hmac_sha256(secret, rawBody))
Debit:
{
"operation": "debit",
"providerTxId": "mock-bet-1",
"providerRoundId": "mock-round-1",
"externalUserId": "session-token",
"amount": "10.00",
"currency": "GOLD",
"gameId": "slot-1"
}
Success:
{
"status": "ok",
"balance": "90.00",
"currency": "GOLD",
"transactionId": "core-transaction-id",
"walletTransactionId": "wallet-transaction-id",
"duplicate": false
}
SoftSwiss-style
Config:
{ "secret": "softswiss-secret" }
Signature: X-REQUEST-SIGN = hex(hmac_sha256(secret, rawBody))
The adapter accepts a single action object or an actions array for combined bet/win calls.
{
"action": "play",
"transactionId": "sw-bet-1",
"roundId": "sw-round-1",
"playerId": "session-token",
"amount": "5.00",
"currency": "GOLD"
}
Errors are returned with HTTP status plus { "status": "error", "code": "INSUFFICIENT_FUNDS" }.
Pragmatic-style
Config:
{ "secret": "pragmatic-secret" }
Signature: form field hash = md5(sorted_params_without_hash + secret).
action=bet&transactionId=pp-bet-1&roundId=pp-round-1&playerId=session-token&amount=7.50¤cy=GOLD&hash=...
Responses always use HTTP 200 with numeric error:
{ "error": 0, "description": "OK", "balance": "92.50", "transactionId": "core-transaction-id", "duplicate": false }
EvoPlay-style
Config:
{ "projectKey": "evoplay-key" }
Signature: signature = hex(hmac_sha256(projectKey, sorted_ordered_fields)).
{
"jsonrpc": "2.0",
"method": "win",
"id": "rpc-1",
"params": {
"transactionId": "evo-win-1",
"roundId": "evo-round-1",
"playerId": "session-token",
"amount": "12.00",
"currency": "GOLD",
"signature": "..."
}
}
Operations And Reconciliation
Admin tooling exposes:
- Provider CRUD, activation kill switch, config validation, and test action.
- Session token creation for staging and certification.
- Round and wallet transaction explorers with raw payload access.
- Discrepancy queue with resolve/write-off actions.
- Health summary for active providers, callback decisions, and open discrepancies.
Automated reconciliation covers stuck rounds, wallet transaction versus core ledger drift, and provider statement diffing into matched, missing-ours, missing-theirs, and amount-mismatch buckets.
Callback signing update (2026-10-29)
EvoPlay-style JSON callbacks authenticate the method and request ID together with typed parameters. Sign the UTF-8 encoding of JSON.stringify(['v1', body.method, body.id ?? null, sortedParamsWithoutSignature]) with HMAC-SHA256 using projectKey. Sort top-level parameter keys lexicographically and exclude only signature. Preserve parameter value types. Params-only signatures are rejected. Coordinate this update with the provider before enabling callbacks.
Mock callbacks are available only in development/test environments. Replace any previously saved placeholder secret before enabling a real provider. Production signing secrets require at least 32 bytes. Provider mutations require a super administrator; reads require administrator and finance-view permission.