Skip to content

Currency Shop

This recipe builds an in-game shop with the first-party store module. Players pay with a wallet currency and receive items, currencies or counters.

  1. On the Counters page, create a currency counter, for example gold, and ship it in a release. Wallet operations need the counter to be in the environment’s latest release.
  2. Open Modules and install store. Its dependencies (reward, inventory, items) are installed with it.
  3. In Items, create the goods you sell, for example health_potion.
  4. In Store → Offers, create an offer:
    • Name and slug (the slug is what the client buys by).
    • Price — a currency and an amount. Leave it empty for a free offer.
    • Reward — items, currencies and counters the player receives.
    • Schedule — when the offer is on sale; empty means always.
    • Segment — which players see it; empty means everyone.
    • Max purchases — per player; empty means unlimited.
    • Position — order in the shop, lowest first.

With store installed, generate a typed client for your game (see Generated module clients):

Terminal window
rudder client generate --lang ts --out src/rudder.modules.ts
rudder client generate --lang csharp --out Assets/Rudder/RudderModules.g.cs

list returns the offers the calling player can buy right now: on sale, matching the segment, and under the purchase limit.

import { modules } from './rudder.modules';
const m = modules(client);
const { offers } = await m.store.list({});
for (const offer of offers) {
const price = offer.price ? `${offer.price.amount} ${offer.price.currency}` : 'free';
console.log(offer.name, price, `${offer.purchases}/${offer.maxPurchases || ''}`);
}

Each offer has slug, name, image, position, price ({currency, amount} or null for a free offer), reward ({currencies, items, counters}), maxPurchases (0 means unlimited) and purchases (the player’s purchase count).

import { RudderModuleError } from '@rudder/sdk';
import { type StoreBuyError } from './rudder.modules';
try {
const { offer, boughtAt } = await m.store.buy({ offerSlug: 'health_pack' });
await client.player.reload(); // refresh wallet balances now
} catch (error) {
if (error instanceof RudderModuleError) {
const code = error.code as StoreBuyError;
// offer_not_found, offer_not_available, purchase_limit_reached,
// insufficient_funds or another kernel code
}
}

A purchase runs in one transaction: the wallet is debited, the reward is granted, the purchase is recorded and a store.purchased event is emitted. If any step fails, nothing is written. buy returns the purchased offer and boughtAt.

The SDK sends an idempotency key with the call and retries network errors with the same key, so a purchase is never charged twice because of a retry.

Wallet balances are on the player profile: client.player.value?.wallets (TypeScript) or client.Player.Value?.Wallets (C#).

Inventory rows live in the player resource inventory, which game clients cannot read directly. See Wallet & Inventory for exposing it through a module.

The quests module listens to store.purchased: purchase_offer objectives count purchases of an offer and purchase_item objectives count purchases whose reward contains an item.