Counters
What is a counter?
Section titled “What is a counter?”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
currencycounter. - A quest objective of type
counteradds up a counter’s increments. - A reward (granted by the
rewardmodule) 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.
Kinds: currency, number
Section titled “Kinds: currency, number”Every counter has a kind, chosen at creation and immutable:
| Kind | Where player values live | Visible in wallet | Audit trail |
|---|---|---|---|
currency | Player wallet | Yes (balance in player profile) | Yes, every change is logged |
number | Separate counter values | No (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.
Authoring counters
Section titled “Authoring 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. - Kind —
currencyornumber. Also immutable after creation. - Increment only —
numbercounters only. Rejects every debit. Editable after creation. - Visible to game clients —
numbercounters only, on by default (clientVisiblein the API). Visible counters appear in the player profile’scounters; hidden ones stay available to modules only. Editable after creation.
Other rules:
- Counters are edited in the staging environment;
prodchanges 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.
How the runtime applies counters
Section titled “How the runtime applies counters”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:
- 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.
- Routes the write:
currencykind updates the wallet (with an audit entry);numberkind updates a separate per-player value. - On positive deltas only, emits
counter.incrementedwith the slug and delta in the same transaction. Module event handlers (for examplequests.onCounterandbattlepass.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.
Reading counter values
Section titled “Reading counter values”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 withCounters.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.
Edge cases and behavior to know
Section titled “Edge cases and behavior to know”- 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.