TANITARDevelopers
GuidesRewards

Wallets and the ledger

Read a player's balances and post value to their wallet directly, with every change recorded as one balanced ledger entry.

A player holds value in wallets. Each wallet is identified by the player, a walletScope and, for game wallets, a gameSlug:

walletScopegameSlugHolds
platformnullCurrencies usable across your whole tenant
gamethe game's slugCurrencies tied to one game

A wallet holds a list of balances, one per currency key. It is created the first time value is posted to it, so a player with no activity has no wallets.

Every change to a balance is a ledger posting: a transfer with its own transferId, recorded as balanced ledger entries and applied in the same database transaction as the new balance. A balance therefore always agrees with the sum of its ledger entries. Each posting also raises the wallet's revision and publishes reward.wallet.balance-changed (see the event catalog).

Read a player's wallets

GET /v1/wallets/:playerId with scope rewards:read.

curl "$REWARD_API_URL/v1/wallets/player-7f3a" \
  -H "Authorization: Bearer $TANITAR_API_KEY"
[
  {
    "playerId": "player-7f3a",
    "walletScope": "platform",
    "gameSlug": null,
    "balances": [
      { "currencyKey": "red", "amount": 1250 },
      { "currencyKey": "blue", "amount": 40 }
    ],
    "revision": 12,
    "updatedAt": "2026-09-30T10:14:02.118Z"
  }
]
  • The response is an array with every wallet the player holds in your tenant. It is not paginated.
  • A player with no wallets returns []. Reading never creates a wallet or a ledger entry.
  • The tenant always comes from your API key. A player of another tenant reads as [], exactly like a player who does not exist.
  • Amounts are integers in the currency's smallest unit.

Players read their own wallets through the Player surface, GET /player/v1/wallets, which returns the same shape.

Post value directly

Most value reaches players through grants: a mission, chest, battle-pass tier or award settles once and records why. When your own system is the source of the value (a purchase in your store, a migration from an earlier programme, a correction), you can post to a wallet directly with POST /v1/wallet-value/post and scope rewards:mutate.

FieldRule
playerIdThe player whose wallet changes
walletScopeplatform or game
gameSlugRequired when walletScope is game
currencyKeyA currency you have published
amountA non-zero integer: positive credits, negative debits
reasonFree text, stored on the ledger entry
sourceOne of api-command, broker-event, currency-exchange, migration, operator-recovery
operationKeyYour identifier for this operation, unique per intended change
curl -X POST "$REWARD_API_URL/v1/wallet-value/post" \
  -H "Authorization: Bearer $TANITAR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"playerId":"player-7f3a","walletScope":"platform","currencyKey":"red","amount":500,"reason":"Launch week bonus","source":"api-command","operationKey":"launch-bonus:player-7f3a"}'

The response carries the transferId and the resulting wallet.

Exactly once, by operationKey

The operationKey is the posting's identity on the ledger, separate from the request's Idempotency-Key:

  • Sending the same operationKey with the same command again returns the original posting. The balance moves once.
  • Sending the same operationKey with a different command (another amount, player or currency) is refused with 409 VALUE_OPERATION_CONFLICT.

Derive operationKey from the business event in your system (an order id, a migration row id) so a retry after a timeout can never pay twice.

Refusals

StatuserrorCodeCause
400VALUE_INPUT_INVALIDThe amount is zero, the posting would take a balance below zero or past the safe-integer range, or a game wallet was named without gameSlug
404CURRENCY_UNAVAILABLEThe currency does not exist or is not published
409VALUE_OPERATION_CONFLICTThe operationKey was already used for a different command
409WALLET_REVISION_CONFLICTThe wallet kept changing concurrently and the posting could not commit; retry with the same operationKey

A refused posting writes nothing: no balance changes and no ledger entry is recorded.

A direct posting is not a grant. It creates no grant record, so it does not appear in GET /v1/reward-grants/:playerId, and it publishes reward.wallet.balance-changed but not reward.grant-applied. If you want a delivered reward to be traceable as an award, use an award definition with a value and grant it, as described in Award kinds and awards.

On this page