View as Markdown

Build a store with dynamic pricing and time-limited offers

In a live game, your store shouldn’t stay the same for every player. A shop that adapts to how players behave and to the time of year drives more revenue and stays relevant for longer. Satori, the LiveOps platform by Heroic Labs, lets your team make changes like these on the fly by keeping your game’s configuration separate from its code.

This guide shows you how to transform an ordinary, static shop into a configurable, dynamic one. By the end, you’ll have a storefront whose prices, offers, and available items change per player and on a schedule, all managed remotely from Satori while your game is running.

The Hiro Store showing weapons and items at their full base prices
The base Hiro Store project allows the player to spend currency and buy items

Before you begin #

To follow the guide and see the changes while running the project on your local machine, you’ll need:

  • A running Nakama server with Hiro installed.
  • A hosted Satori instance on Heroic Cloud.
  • A Satori API key. In the Satori Console, go to Settings > API keys.
  • Your Satori instance URL and port, for example https://yourinstance.satoricloud.io on port 443.

This guide builds on top of the Hiro Store sample project. To see the final version with everything in place, download the DynamicStore project. Not every boilerplate UI change is covered in this guide. Instead, use git diff in the sample-projects repo to see every change between the base project and the updated version:

1
git diff --no-index system-templates/UnityHiroStore guides/DynamicStore

Understand what makes a store dynamic #

Building a dynamic store pulls together several tools from across the Heroic Labs stack. Each is useful on its own, but together they unlock deep personalization that none of them could deliver alone. The techniques you’ll learn here carry over to any other part of your game that benefits from player customization.

These are the core components at work:

  • Hiro Economy defines your store’s catalog. It reads a base catalog from a config file (base-economy.json) and returns items, prices, and rewards to the client.
  • Personalizers sit between each Hiro system and its config. A personalizer can rewrite the config per request before Hiro serves it. Hiro ships with the SatoriPersonalizer, which pulls its remote config from Satori.
  • Satori feature flags define the overrides that apply to your base configs. Every Hiro system maps to a single flag, and the Economy system uses the flag named Hiro-Economy. Its value is JSON that merges onto your base economy config.
  • Satori audiences and live events decide which flag value a given player receives. An audience groups players by their properties and behavior. A live event overrides a flag for a scheduled window. Either one can hand a different Hiro-Economy value to different players.

You’ll notice that most of the code in this guide simply wires up the connection to Satori. Once that’s in place, the actual customization is driven from the Satori Console. That separation of configuration from code is central to how Hiro and Satori work together. See Thinking in Hiro to read about this design.

Base economy configuration #

Everything the store personalizes builds on this base catalog. It defines the player’s starting currencies and every store item. The variant and live-event overrides later in the guide merge onto these definitions:

Connect the project to Satori #

Before Satori can tailor the store to each player, your game has to talk to it. This section adds a small amount of code that connects the project: it registers the personalizer on the server and authenticates players so the client and server recognize them the same way. By the end, the server personalizes the store on every refresh, and everything after this is configuration you drive from the Console.

Step 1

Register the personalizer on the server #

The SatoriPersonalizer is what lets Satori shape any Hiro system, including your store. Add it after hiro.Init:

main.go
1
2
3
4
systems.AddPersonalizer(hiro.NewSatoriPersonalizer(ctx,
    hiro.SatoriPersonalizerPublishAuthenticateEvents(),
    hiro.SatoriPersonalizerPublishEconomyEvents(),
))

The two options passed into NewSatoriPersonalizer are:

OptionWhat it does
SatoriPersonalizerPublishAuthenticateEvents()Creates each player’s Satori identity server-side on login.
SatoriPersonalizerPublishEconomyEvents()Forwards Hiro economy events, such as purchases, to Satori, so you can build audiences on spending behavior.

The personalizer works with the Economy system automatically, through the Hiro-Economy flag. To go deeper on how personalizers rewrite Hiro configs, see Personalizers.

Step 2

Authenticate to Satori on the client #

Authenticate the client’s Satori session with the Nakama user ID, so the client and server refer to the same player. For the full authentication walkthrough, see the Satori Unity client guide.

First, add your Satori connection settings to HiroStoreCoordinator.cs so you can edit them in the Inspector view:

HiroStoreCoordinator.cs
1
2
3
4
5
[Header("Satori Settings")]
[SerializeField] private string satoriScheme = "https";
[SerializeField] private string satoriHost = "yourinstance.satoricloud.io";
[SerializeField] private int satoriPort = 443;
[SerializeField] private string satoriApiKey = "your-api-key";

Then, in CreateSystemsAsync(), create a Satori client, wrap it in Hiro’s SatoriSystem, and add it to the Systems container after the existing systems:

HiroStoreCoordinator.cs
1
2
3
4
var satoriClient = new Satori.Client(satoriScheme, satoriHost, satoriPort, satoriApiKey,
    Satori.UnityWebRequestAdapter.Instance);
var satoriSystem = new SatoriSystem(logger, satoriClient, SatoriAuthorizerFunc(nakamaSystem));
systems.Add(satoriSystem);

Register SatoriSystem after NakamaSystem. Hiro initializes systems in registration order, so this ordering guarantees the Nakama session exists by the time the Satori authorizer runs.

Finally, add the authorizer. It authenticates a fresh Satori session on every login using the player’s Nakama user ID:

HiroStoreCoordinator.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static SatoriSystem.AuthorizerFunc SatoriAuthorizerFunc(NakamaSystem nakamaSystem)
{
    return async client =>
    {
        var userId = nakamaSystem.Session.UserId;
        return await AuthenticateSatoriAsync(client, userId);
    };
}

public static async Task<Satori.ISession> AuthenticateSatoriAsync(Satori.IClient client, string userId)
{
    // Send platform and language so you can build audiences on them too.
    var defaultProperties = new Dictionary<string, string>
    {
        { "platform", Application.platform.ToString() },
        { "language", Application.systemLanguage.ToString() }
    };

    return await client.AuthenticateAsync(userId, defaultProperties);
}
Authenticate a fresh session each time the game starts instead of re-using tokens to restore a previous one. The player’s identity stays consistent across restarts because it’s attached to the Nakama user ID you pass in. Starting a new session each boot keeps session metrics accurate. For the reasoning behind this, see Sessions.
Step 3

Report purchases to Satori #

The server does the actual personalizing: on each refresh it merges the player’s flag value onto the base config and returns the result. The client reports purchases to Satori for analytics and audience segmentation.

First, hand the controller its connection to Satori. Add a _satoriSystem field and an optional constructor parameter:

StoreController.cs
1
2
3
4
5
6
7
private readonly ISatoriSystem _satoriSystem;

public StoreController(..., ISatoriSystem satoriSystem = null)
{
    // existing code
    _satoriSystem = satoriSystem;
}

Pass the system in where the controller is created:

StoreViewBehaviour.cs
1
2
3
4
5
6
// Optional: when null, the store still works; purchase reporting and the
// event countdown are skipped.
var satoriSystem = _coordinator.GetSystem<SatoriSystem>();

Controller = new StoreController(nakamaSystem, economySystem, BuildItemIconDictionary(),
    BuildCurrencyIconDictionary(), _defaultItemIcon, satoriSystem);

The Starter Pack is a one-time offer: every player sees it at first, but it should change once they buy it. To make that happen, record the purchase on the player’s Satori identity. After any successful purchase, MarkItemPurchasedAsync sets a purchased_<itemId> custom property and asks Satori to recompute the player’s audiences right away:

StoreController.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private async Task MarkItemPurchasedAsync(IEconomyListStoreItem item)
{
    if (_satoriSystem == null) return;

    try
    {
        await _satoriSystem.UpdatePropertiesAsync(
            new Dictionary<string, string>(),
            new Dictionary<string, string> { { $"purchased_{item.Id}", "true" } },
            recompute: true);
    }
    catch (Exception e)
    {
        Debug.LogWarning($"Failed to update Satori purchase property: {e.Message}");
    }
}

Call it from PurchaseItemAsync after the purchase succeeds:

StoreController.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public async Task<IEconomyPurchaseAck> PurchaseItemAsync(IEconomyListStoreItem item)
{
    if (item == null)
        throw new Exception("No item selected");

    var result = await _economySystem.PurchaseStoreItemAsync(item.Id);

    await MarkItemPurchasedAsync(item);

    return result;
}

Setting purchased_starter_pack moves the player into a Starter-Pack-Buyers audience, which you’ll create in the Console, and their store changes to match.

This is a reusable LiveOps lever. The client reports a property for every item bought, but Satori only keeps the ones you register in the Console. To launch an offer for buyers of any item, register that property and build an audience on it, with no client change and no new build. You’ll do exactly that for the Starter Pack later in this guide, so you’ll see the whole flow end to end.

Step 4

Show the live event banner #

The base project already renders badges, featured items, and themes from each item’s properties, so there isn’t much more UI work to be done. The one addition is the banner and countdown that appear while a live event runs. Find the banner element in StoreUI.uxml and its wiring in StoreView.cs.

Configure offers in the Satori Console #

That’s all the code. From here on, you shape the entire player experience from the Satori Console, with no further changes to your game. This is the payoff of keeping configuration separate from code: your team can add offers, target segments, and schedule sales without an engineer or a new build.

Everything below happens in the Console for the project your API key belongs to.

Step 1

Set the Hiro-Economy flag value #

By default, Satori provides a feature flag named Hiro-Economy. Its value is the JSON that Satori delivers to Hiro, which merges it onto your base economy config. Set its default value to an empty object, so players who match no audience or event see the base store untouched:

1
{}
Step 2

Register the purchase property #

In the previous section, the client set a purchased_starter_pack property after purchase. For that to take effect, the property has to be whitelisted in Satori first: every custom property must be registered in the Taxonomy before the client can set it. Go to Taxonomy > Properties, and create a custom property named exactly purchased_starter_pack with a Boolean schema.

Satori Console Create Custom Property modal with the name purchased_starter_pack, a description, and a Boolean schema validator
Registering the purchased_starter_pack custom property
Satori silently ignores updates to unregistered property names, so a typo here means the segment never triggers, with no error to point at. If an offer isn’t showing up later, check this spelling first.
Step 3

Create the Starter-Pack-Buyers audience #

Create an audience named Starter-Pack-Buyers that matches players who have the property set:

1
PropertiesCustom("purchased_starter_pack", false) == true

The client sets purchased_starter_pack to true when a player buys the Starter Pack, so buying the pack is what moves them into this audience.

Satori Console audience overview for Starter-Pack-Buyers showing its filter PropertiesCustom purchased_starter_pack equals true
The Starter-Pack-Buyers audience and its filter
Step 4

Add a variant to the Hiro-Economy flag #

Add a variant to the Hiro-Economy flag and assign it to the Starter-Pack-Buyers audience. Players in that audience receive this value instead of the default. It re-prices the featured pack: the base config shows the Starter Pack with a SALE badge at 2500 coins; this variant renames it to Value Pack, raises the price to 4000 coins, and clears the badge, so the introductory deal is gone once the player has bought it:

starter-pack-buyers-variant.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "store_items": {
    "starter_pack": {
      "name": "Value Pack",
      "description": "Amazing starter bundle with 6000 gems!",
      "category": "currency",
      "cost": {
        "currencies": {
          "coins": 4000
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "gems": {
              "min": 8000,
              "max": 8000
            }
          }
        }
      },
      "additional_properties": {
        "featured": "true",
        "theme": "primary",
        "badge": ""
      }
    }
  }
}

When Satori serves this value, it’s combined with your base config:

  • Items you don’t include keep their base definition.
  • Items you include are matched to the base item by key and override it, so list every field the item should keep, not only the ones you’re changing.
The Hiro-Economy flag in the Satori Console showing its default value and the Starter-Pack-Buyers variant
The Hiro-Economy flag with the Starter-Pack-Buyers variant added
Step 5

Create the Shopping-Event live event #

Audiences change the store based on who a player is; a live event changes it for everyone in its scope, on a schedule. Create one to put the whole item catalog on sale for a limited window.

In the Satori Console, create a live event. The Console offers Hiro-aware templates, such as dynamic pricing and timed offers, as a starting point. Select Timed Offer.

Satori Console live event template picker showing Blank Template, Dynamic Pricing, Timed Offer, Solo Timed Event, and Offer Wall options
Live event templates in the Satori Console
  1. Name the event Shopping-Event.
  2. Leave the Metrics as-is and select Next.
  3. Keep the default settings for Target Audience: the live event is delivered to all players.
  4. Under Feature Flags is where you’ll define the override for the store. Select Hiro-Economy as the flag to override and paste in the JSON below. Select Next.
  5. Leave Live Event Value empty, it won’t be used in this project.
  6. For Scheduling, choose today’s date and any end time. Leave Repeat disabled.

In the live event configuration, each item carries event_id set to the event’s exact name, which is how the client finds the event to drive the banner countdown: During the live event, all items in the store are available at half price.

A live event’s flag override replaces the value a player would otherwise get from an audience variant, it doesn’t stack. While Shopping-Event runs, the Value Pack re-pricing is suspended and returns when the event ends. To keep segment pricing during an event, include those items in the override too.
Satori Console Shopping-Event live event, running, with a Hiro-Economy flag override on its Feature Flags tab
The Shopping-Event live event overriding the Hiro-Economy flag

Try it out #

With the code in place and the Console configured, run the project and watch the store respond. Open the Main scene and enter Play mode. To download a copy of the full working implementation, see the DynamicStore project.

See the one-time offer #

The store opens in its default state, showing only what’s in the base config: the featured Starter Pack is on sale at 2500 coins, and the Items tab lists everything at full price.

The base Hiro Store project displaying two rows of currency that the player can purchase as well as a starter pack
The base Hiro Store with everything at their default prices

Buy the Starter Pack. The client records the purchase as the purchased_starter_pack property and asks Satori to recompute audiences, which moves the player into Starter-Pack-Buyers.

Select Refresh. The featured slot rebuilds from the variant: the pack is now the Value Pack at 4000 coins with the sale badge gone.

The store Currency tab after purchase, with the featured pack renamed Value Pack priced at 4000 coins and no sale badge
After the purchase: the featured pack is re-priced as the Value Pack
The property update requests an immediate recompute, so the new offer usually appears on the first refresh. If it doesn’t, refresh once more.

To confirm the round-trip, open the player’s identity in the Satori Console and check its events: a _propertiesUpdate event carries purchased_starter_pack set to true.

Satori Console player events list with a _propertiesUpdate event expanded to show purchased_starter_pack set to true
The purchase recorded as a custom property on the player's identity

If you added the optional account-switch alignment, switch to a fresh account to replay the demo from scratch. The new Nakama user is a new Satori identity with no purchase history, so the store returns to the base config.

See the limited-time sale #

While Shopping-Event is running, return to the game and select the Items tab. Hit Refresh to see that every item drops to half price, and a “Shopping Event - Everything on sale!” banner appears in the bottom-left corner with a live countdown.

The store Items tab during the sale, every item at half price with a Shopping Event banner and countdown in the bottom-left corner
The Items tab during the sale, with the event banner and countdown

When the event ends, the next refresh restores regular prices and the banner disappears, with no deploy at either end. You can prematurely “end” the event by editing the start date of the event in the Satori Console to confirm this yourself.

Recap #

Your store’s catalog, prices, and available offers are now controlled from Satori while the game runs:

  • Buying the Starter Pack moves a player into the Starter-Pack-Buyers audience, whose variant re-prices the featured pack from an intro sale to its regular Value Pack price.
  • A live event puts the item catalog on sale for a scheduled window, complete with an on-screen banner and countdown, then restores prices when it ends.

The same pattern extends anywhere the base config reaches. Register another purchase property to target buyers of a different item, build audiences from other player properties the client sends, or schedule more live events, all from the Console and all without touching base-economy.json.

See also #