Skip to content

Counters

A counter is a named, per-player integer identified by a stable slug like coins, xp, or boss_kills. Counters are part of the kernel, and modules reference them by slug:

  • A store offer’s price is paid in a currency counter.
  • A quest objective of type counter adds up a counter’s increments.
  • A reward (granted by the reward module) can credit currencies and counters.
  • A battle pass season maps counters to XP (xpSources).

Crediting a counter is one server-side operation: it updates the value (or the wallet balance for currencies) and emits the counter.incremented event in the same transaction, which is how quests and the battle pass advance.

Every counter has a kind, chosen at creation and immutable:

KindWhere player values liveVisible in walletAudit trail
currencyPlayer walletYes (balance in player profile)Yes, every change is logged
numberSeparate counter valuesNo (in counters of the player profile when visible to game clients)No

The kind only controls storage. Pick currency for money the player spends; pick number for progress numbers such as wins or XP.

A number counter can be marked increment only — a dedicated flag on the counter, next to name, slug, and kind. Any negative delta against such a counter is rejected with an error instead of debiting it. The flag has no effect on currency counters.

In the dashboard at https://app.rudder.build/, open your project and go to Counters:

  • Name — display name used in dashboard dropdowns.
  • Slug — the identity every feature references. Derived from the name (lowercased) if left empty. Must match ^[a-z][a-z0-9_:-]{0,63}$ and is unique per project and environment. Immutable after creation.
  • Kindcurrency or number. Also immutable after creation.
  • Increment onlynumber counters only. Rejects every debit. Editable after creation.
  • Visible to game clientsnumber counters only, on by default (clientVisible in the API). Visible counters appear in the player profile’s counters; hidden ones stay available to modules only. Editable after creation.

Other rules:

  • Counters are edited in the staging environment; prod changes through promotion. A counter only takes effect in the runtime after a release that contains it has completed.
  • Delete archives a counter. Archived counters stay listed in the dashboard but are excluded from new releases. Existing player values (wallet balances for archived currencies) are preserved.

Counters change only inside module functions (Counters.Increment in the module SDKs, or reward.grant) and through admin wallet adjustments. There is no SDK endpoint that increments a counter directly; game clients call a module function that does it.

When a counter is incremented, the server:

  1. Looks the slug up in the environment’s latest release. Unknown slug → silent no-op with a warning in the server logs, not an error. This is why unreleased counters “don’t work” without failing.
  2. Routes the write: currency kind updates the wallet (with an audit entry); number kind updates a separate per-player value.
  3. On positive deltas only, emits counter.incremented with the slug and delta in the same transaction. Module event handlers (for example quests.onCounter and battlepass.onCounter) run in savepoints: a failing handler is logged and does not fail the increment.

Debits are guarded: a debit that would take the value below zero fails with insufficient_funds, and a number counter marked increment-only rejects every debit with counter_only_increment.

Both kinds are part of the player profile (GET /sdk/v1/player/information), which the SDKs keep as the player state:

  • Currency-kind counters are in wallets, where every released currency appears, zero-filled if the player never received it. See Wallet & Inventory.
  • Number-kind counters are in counters, a map of slug to value, when Visible to game clients is on. Hidden counters are left out; modules read them with Counters.Get.
const wins = client.player.value?.counters?.wins ?? 0;
var profile = await client.Player.LoadAsync();
long wins = 0;
profile.Counters?.TryGetValue("wins", out wins);

Counter changes bump the profile revision, so SDK clients pick them up within one sync interval (about 30 seconds). Call reload() / ReloadAsync() on the player state after a module call when you need the new value right away.

  • Unreleased or misspelled slugs silently do nothing at runtime (warn log server-side). If a reward doesn’t land, check that the counter exists, is active, and is in the latest release.
  • Slug and kind are frozen at creation. To “rename” a currency, create a new counter and migrate: grant the new slug, archive the old one.