Skip to content

Wallet & Inventory

Rudder tracks two kinds of player-owned goods:

  • Wallet — part of the kernel. One integer balance per currency; currencies are currency-kind counters.
  • Inventory — provided by the first-party inventory module. One row per player and item in the player resource inventory (itemSlug, amount); items come from the items module.

Both are server-authoritative. Game clients only read them; every change happens in a module function or an admin operation inside a transaction, so a hacked client cannot mint currency or items.

In the dashboard at https://app.rudder.build/, open your project:

  1. Counters — create a currency counter for each currency (e.g. coins, gems) and ship it in a release. Wallets list only currencies from the environment’s latest release.
  2. Modules — install inventory (it brings items), then create items in Items. Item rows are live as soon as you save them.

Balances are part of the player profile. Every released currency-kind counter appears in the response — a player who never received that currency gets a virtual 0 balance, so you don’t need to special-case missing wallets.

// client.player is a state object, loaded at login and kept fresh by sync
const profile = await client.player.load();
for (const wallet of profile.wallets ?? []) {
console.log(wallet.currency, wallet.balance);
}
// Or subscribe to updates (revision sync or reload() refresh it)
client.player.onChange(({ value }) => {
const coins = value?.wallets?.find((w) => w.currency === 'coins');
renderBalance(coins?.balance ?? 0);
});

Number-kind counters never appear in the wallet; visible ones are in the profile’s counters map. See Counters.

Inventory rows are module data in the player resource inventory. Game clients read them with the inventory.list client function through a generated module client: await modules(client).inventory.list({}) returns {items: [{itemSlug, amount, item}]}, where item carries the display data from items (properties is JSON text) and is null for an unknown slug. Rewards returned by quests.claim and offers returned by store.list already carry item slugs and amounts.

There is no client-side grant call. Modules pay out through the reward module, which applies currencies, items and counters in one call:

  • Storestore.buy debits the offer’s price and grants its reward in one transaction.
  • Questsquests.claim grants a completed quest’s reward.
  • Battle passbattlepass.claim grants a tier’s reward.
  • Leaderboards — the scheduled reset grants rank rewards.

Your own modules can call reward.grant (declare reward in deps) or use the kernel wallet directly. See Functions runtime.

Open Players → player detail in the dashboard. The wallet tab shows balances and transaction history; Adjust applies a signed delta with a mandatory reason. The inventory module adds its rows and Grant item / Remove item actions to the same page.

From a backend, call the /game/v1 HTTP API with the environment’s admin key (X-API-Key): POST /game/v1/players/wallet/adjust (a batch with an optional idempotency key) and GET /game/v1/players/{playerId}/wallet/history for the wallet, /game/v1/resources/{slug}/rows for module data, and POST /game/v1/modules/{module}/{fn} for admin functions such as inventory.adminGrant.

  • Balances never go negative. A debit that would drop a balance below zero fails with insufficient_funds and changes nothing.
  • Crediting auto-creates the wallet — the first grant of a currency works without any setup call.
  • Every wallet change is audited. The transaction history (amount, balance before/after, reason, source) is visible in the dashboard player view and through GET /game/v1/players/{playerId}/wallet/history.
  • Currencies come from releases. A player’s wallet list is filled with a zero balance for every currency in the latest release.
  • Client state is a cache. The SDKs refresh the profile through revision sync; call client.player.reload() / client.Player.ReloadAsync() after a module call that changes balances. The server is the source of truth.