# Build a dynamic store

**URL:** https://heroiclabs.com/docs/hiro/guides/personalizer/dynamic-store/
**Keywords:** dynamic store, virtual store, personalizer, satoripersonalizer, dynamic pricing, seasonal offers, time-limited offers
**Categories:** hiro, personalizer, guides

---


# 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](../../../../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.

{{< screenshot
src="images/store_items_default.png"
alt="The Hiro Store showing weapons and items at their full base prices"
caption="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](../../../../nakama/getting-started/install/docker/) server with [Hiro](../../../concepts/getting-started/install/) installed.
- A hosted Satori instance on [Heroic Cloud](https://cloud.heroiclabs.com).
- 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](../../../../sample-projects/unity/hiro-store/). To see the final version with everything in place, download the [DynamicStore](https://github.com/heroiclabs/sample-projects/tree/main/guides/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:

```bash
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](../../../concepts/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](../../../../satori/concepts/remote-configuration/understand-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](../../../concepts/introduction/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:

{{< accordion title="Show base-economy.json" >}}

```json
{
  "initialize_user": {
    "currencies": {
      "coins": 1000,
      "gems": 1000
    }
  },
  "store_items": {
    "starter_pack": {
      "name": "Starter Pack",
      "description": "Starter pack for new players. Great value!",
      "category": "currency",
      "cost": {
        "currencies": {
          "coins": 2500
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "gems": {
              "min": 8000,
              "max": 8000
            }
          }
        }
      },
      "additional_properties": {
        "featured": "true",
        "badge": "SALE",
        "theme": "primary"
      }
    },
    "gems_500": {
      "name": "500 Gems",
      "description": "500 gems - Free!",
      "category": "currency",
      "cost": {
        "currencies": {}
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "gems": {
              "min": 500,
              "max": 500
            }
          }
        }
      }
    },
    "gems_1000": {
      "name": "1000 Gems",
      "description": "1000 gems bundle",
      "category": "currency",
      "cost": {
        "currencies": {
          "coins": 1000
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "gems": {
              "min": 1000,
              "max": 1000
            }
          }
        }
      }
    },
    "gems_2000": {
      "name": "2000 Gems",
      "description": "2000 gems bundle",
      "category": "currency",
      "cost": {
        "currencies": {
          "coins": 2000
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "gems": {
              "min": 2000,
              "max": 2000
            }
          }
        }
      }
    },
    "gems_5000": {
      "name": "5000 Gems",
      "description": "5000 gems bundle",
      "category": "currency",
      "cost": {
        "currencies": {
          "coins": 5000
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "gems": {
              "min": 5000,
              "max": 5000
            }
          }
        }
      }
    },
    "coins_500": {
      "name": "500 Coins",
      "description": "500 coins - Free!",
      "category": "currency",
      "cost": {
        "currencies": {}
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "coins": {
              "min": 500,
              "max": 500
            }
          }
        }
      }
    },
    "coins_1000": {
      "name": "1000 Coins",
      "description": "1000 coins bundle",
      "category": "currency",
      "cost": {
        "currencies": {
          "gems": 1000
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "coins": {
              "min": 1000,
              "max": 1000
            }
          }
        }
      }
    },
    "coins_2000": {
      "name": "2000 Coins",
      "description": "2000 coins bundle",
      "category": "currency",
      "cost": {
        "currencies": {
          "gems": 2000
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "coins": {
              "min": 2000,
              "max": 2000
            }
          }
        }
      }
    },
    "coins_5000": {
      "name": "5000 Coins",
      "description": "5000 coins bundle",
      "category": "currency",
      "cost": {
        "currencies": {
          "gems": 5000
        }
      },
      "reward": {
        "guaranteed": {
          "currencies": {
            "coins": {
              "min": 5000,
              "max": 5000
            }
          }
        }
      }
    },
    "loot_box": {
      "name": "Loot Box",
      "description": "A box filled with random items and currencies.",
      "category": "items",
      "cost": {
        "currencies": {
          "gems": 250
        }
      },
      "reward": {
        "weighted": [
          {
            "currencies": {
              "coins": {
                "min": 1000,
                "max": 2000,
                "multiple": 100
              }
            },
            "weight": 6
          },
          {
            "items": {
              "golden_key": {
                "min": 1,
                "max": 1
              }
            },
            "weight": 3
          },
          {
            "items": {
              "evil_eye": {
                "min": 1,
                "max": 1
              }
            },
            "weight": 1
          }
        ],
        "total_weight": 10,
        "max_rolls": 1
      },
      "additional_properties": {
        "featured": "true",
        "badge": "DEAL",
        "theme": "secondary"
      }
    },
    "iron_sword": {
      "name": "Iron Sword",
      "description": "A sharp sword made of iron.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 1000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "iron_sword": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    },
    "bomb": {
      "name": "Bomb",
      "description": "Goes BOOM!",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 2000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "bomb": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    },
    "wooden_shield": {
      "name": "Wooden Shield",
      "description": "A basic shield made from reinforced wood.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 2500
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "wooden_shield": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    },
    "mana_potion": {
      "name": "Mana Potion",
      "description": "A vial of blue liquid that restores magical energy.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 3000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "mana_potion": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    },
    "iron_shield": {
      "name": "Iron Shield",
      "description": "A sturdy shield forged from iron. Provides solid protection.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 5000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "iron_shield": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    },
    "golden_key": {
      "name": "Golden Key",
      "description": "An ornate golden key that opens special treasure chests.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 6000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "golden_key": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    },
    "lucky_charm": {
      "name": "Lucky Charm",
      "description": "A four-leaf clover that brings good fortune when used.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 8000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "lucky_charm": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    },
    "evil_eye": {
      "name": "Eye of Sauron",
      "description": "It is said to have the power to control the minds of others.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 10000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "evil_eye": {
              "min": 1,
              "max": 1
            }
          }
        }
      }
    }
  },
  "allow_fake_receipts": true
}
```

{{< /accordion >}}

## 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.

{{< pretitle "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`:

{{< code type="server" filename="main.go" hideable="false" >}}

```go
systems.AddPersonalizer(hiro.NewSatoriPersonalizer(ctx,
    hiro.SatoriPersonalizerPublishAuthenticateEvents(),
    hiro.SatoriPersonalizerPublishEconomyEvents(),
))
```

{{< /code >}}

The two options passed into `NewSatoriPersonalizer` are:

| Option                                          | What 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](../../../concepts/personalizers/).

{{< pretitle "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](../../../../satori/client-libraries/unity/).

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

{{< code type="client" filename="HiroStoreCoordinator.cs" hideable="false" >}}

```csharp
[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";
```

{{< /code >}}

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

{{< code type="client" filename="HiroStoreCoordinator.cs" hideable="false" >}}

```csharp
var satoriClient = new Satori.Client(satoriScheme, satoriHost, satoriPort, satoriApiKey,
    Satori.UnityWebRequestAdapter.Instance);
var satoriSystem = new SatoriSystem(logger, satoriClient, SatoriAuthorizerFunc(nakamaSystem));
systems.Add(satoriSystem);
```

{{< /code >}}

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](/satori/concepts/sessions) on every login using the player's Nakama user ID:

{{< code type="client" filename="HiroStoreCoordinator.cs" hideable="false" >}}

```csharp
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);
}
```

{{< /code >}}

{{< note "important" >}}
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](../../../../satori/concepts/sessions/).
{{< /note >}}

{{< pretitle "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:

{{< code type="client" filename="StoreController.cs" hideable="false" >}}

```csharp
private readonly ISatoriSystem _satoriSystem;

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

{{< /code >}}

Pass the system in where the controller is created:

{{< code type="client" filename="StoreViewBehaviour.cs" hideable="false" >}}

```csharp
// 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);
```

{{< /code >}}

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:

{{< code type="client" filename="StoreController.cs" hideable="false" >}}

```csharp
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}");
    }
}
```

{{< /code >}}

Call it from `PurchaseItemAsync` after the purchase succeeds:

{{< code type="client" filename="StoreController.cs" hideable="false" >}}

```csharp
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;
}
```

{{< /code >}}

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.

{{< pretitle "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.

{{< pretitle "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:

{{< code hideable="false" >}}

```json
{}
```

{{< /code >}}

{{< pretitle "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.

{{< screenshot
src="images/console_modal_create_custom_property.png"
alt="Satori Console Create Custom Property modal with the name purchased_starter_pack, a description, and a Boolean schema validator"
width="75%"
caption="Registering the purchased_starter_pack custom property" >}}

{{< note "important" >}}
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.
{{< /note >}}

{{< pretitle "Step 3" >}}

### Create the Starter-Pack-Buyers audience

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

```bash
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.

{{< screenshot
src="images/console_audience_starter_pack_buyers.png"
alt="Satori Console audience overview for Starter-Pack-Buyers showing its filter PropertiesCustom purchased_starter_pack equals true"
caption="The Starter-Pack-Buyers audience and its filter" >}}

{{< pretitle "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:

{{< code type="server" filename="starter-pack-buyers-variant.json" hideable="false" >}}

```json
{
  "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": ""
      }
    }
  }
}
```

{{< /code >}}

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.

{{< screenshot
src="images/console_hiro_economy_view.png"
alt="The Hiro-Economy flag in the Satori Console showing its default value and the Starter-Pack-Buyers variant"
caption="The Hiro-Economy flag with the Starter-Pack-Buyers variant added" >}}

{{< pretitle "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**.

{{< screenshot
src="images/console_create_live_event_options.png"
alt="Satori Console live event template picker showing Blank Template, Dynamic Pricing, Timed Offer, Solo Timed Event, and Offer Wall options"
width="50%"
caption="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.

{{< accordion title="Show the live event override" >}}

```json
{
  "store_items": {
    "loot_box": {
      "name": "Loot Box",
      "description": "A box filled with random items and currencies.",
      "category": "items",
      "cost": {
        "currencies": {
          "gems": 125
        }
      },
      "reward": {
        "weighted": [
          {
            "currencies": {
              "coins": {
                "min": 1000,
                "max": 2000,
                "multiple": 100
              }
            },
            "weight": 6
          },
          {
            "items": {
              "golden_key": {
                "min": 1,
                "max": 1
              }
            },
            "weight": 3
          },
          {
            "items": {
              "evil_eye": {
                "min": 1,
                "max": 1
              }
            },
            "weight": 1
          }
        ],
        "total_weight": 10,
        "max_rolls": 1
      },
      "additional_properties": {
        "featured": "true",
        "badge": "DEAL",
        "theme": "secondary",
        "event_id": "Shopping-Event"
      }
    },
    "iron_sword": {
      "name": "Iron Sword",
      "description": "A sharp sword made of iron.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 500
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "iron_sword": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    },
    "bomb": {
      "name": "Bomb",
      "description": "Goes BOOM!",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 1000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "bomb": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    },
    "wooden_shield": {
      "name": "Wooden Shield",
      "description": "A basic shield made from reinforced wood.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 1250
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "wooden_shield": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    },
    "mana_potion": {
      "name": "Mana Potion",
      "description": "A vial of blue liquid that restores magical energy.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 1500
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "mana_potion": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    },
    "iron_shield": {
      "name": "Iron Shield",
      "description": "A sturdy shield forged from iron. Provides solid protection.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 2500
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "iron_shield": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    },
    "golden_key": {
      "name": "Golden Key",
      "description": "An ornate golden key that opens special treasure chests.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 3000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "golden_key": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    },
    "lucky_charm": {
      "name": "Lucky Charm",
      "description": "A four-leaf clover that brings good fortune when used.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 4000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "lucky_charm": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    },
    "evil_eye": {
      "name": "Eye of Sauron",
      "description": "It is said to have the power to control the minds of others.",
      "category": "items",
      "cost": {
        "currencies": {
          "coins": 5000
        }
      },
      "reward": {
        "guaranteed": {
          "items": {
            "evil_eye": {
              "min": 1,
              "max": 1
            }
          }
        }
      },
      "additional_properties": {
        "event_id": "Shopping-Event"
      }
    }
  }
}
```

{{< /accordion >}}

{{< note "important" >}}
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.
{{< /note >}}

{{< screenshot
src="images/console_live_event_view.png"
alt="Satori Console Shopping-Event live event, running, with a Hiro-Economy flag override on its Feature Flags tab"
caption="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](https://github.com/heroiclabs/sample-projects/tree/main/guides/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.

{{< screenshot
src="images/store_currency_default.png"
alt="The base Hiro Store project displaying two rows of currency that the player can purchase as well as a starter pack"
caption="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.

{{< screenshot
src="images/store_currency_value_pack.png"
alt="The store Currency tab after purchase, with the featured pack renamed Value Pack priced at 4000 coins and no sale badge"
caption="After the purchase: the featured pack is re-priced as the Value Pack" >}}

{{< note "important" >}}
The property update requests an immediate recompute, so the new offer usually appears on the first refresh. If it doesn't, refresh once more.
{{< /note >}}

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`.

{{< screenshot
src="images/console_identity_events_list.png"
alt="Satori Console player events list with a _propertiesUpdate event expanded to show purchased_starter_pack set to true"
caption="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.

{{< screenshot
src="images/store_items_live_event.png"
alt="The store Items tab during the sale, every item at half price with a Shopping Event banner and countdown in the bottom-left corner"
caption="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

- [Set up built-in Personalizers](../setup-personalizers/)
- [One-time store offers](../one-time-store-offers/)
- [Satori Unity client guide](../../../../satori/client-libraries/unity/)
- [Hiro Store concepts guide](../../../concepts/economy/virtual-store/)
