← Writing
Game Economy · Live-Ops

Building a config-driven live-ops store for a shipped game

TerraGenesis: Landfall · contract work · 2022

A live mobile game's economy is never finished. Prices get retuned, new bundles launch for an event, a limited-time deal runs for a weekend, a starter pack targets new players. The one thing you can't do is ship an app-store build every time the economy changes — review latency alone would kill live-ops. So the real design problem for a store isn't the buy button; it's making merchandising into data: something a designer edits in the inspector and a server can override, with the client as a dumb, reliable renderer. That's the store and economy system I built as contract work on TerraGenesis: Landfall.

Worth noting when this was built — 2022. Turnkey live-ops economy platforms weren't the norm yet, and Unity had not shipped a mature first-party equivalent — its own economy and remote-config services were only just beginning to appear and were nowhere near a standard, drop-in solution. A data-driven store you could retune from a server was something you architected and built yourself, which is exactly what this is.

The data model: a store you author, not code

Everything the store shows is described by a StoreConfig ScriptableObject — an array of StoreCategory and an array of StoreItem, authored entirely in Unity's inspector. A StoreItem is the unit of merchandising: a product id, the store SKU (identical across the Apple and Play stores), a Unity IAP ProductType, a display Order weight, a category, localization keys, and its contents. Its payload is a list of StoreItemContent — a typed reward (hard currency, soft currency, gear, a city bundle, or a link) with an amount. One content is a simple product; more than one makes it a bundle. Adding a new offer means editing an asset, not writing code.

Pricing that adapts to the player

Cost isn't a single number. Each item carries a small ladder — a free amount, an ads amount, then a real cost — and the runtime picks the current price from how many times the player has already bought it:

That whole priority order — free → ads → cost — lives in one computed property, evaluated against a per-day or all-time purchase count depending on the item's recurrence. A "daily" item resets its counter each day, so the same asset can be a free daily reward and a paid impulse buy without any special-casing at the call site.

Time-boxed offers: expiration and reappearance

Limited-time deals need two clocks. An item's expirationDuration defines how long it stays live once a player first sees it; the store computes an expiration timestamp from that first-seen date and renders a live HH:MM:SS countdown, hiding the item once it lapses. A second value, expirationReappearance, controls when a featured deal comes back after expiring — so a weekend offer can cycle without a designer babysitting it. Deals surface in a stable, FIFO order so the merchandising feels intentional rather than random.

Contextual triggers, not just a storefront

The highest-converting offers aren't in the store tab — they meet the player in the moment. So the same data model drives popups with real triggers:

Built for server-side live-ops

The point of making the store data-driven is that data can come from a server. Every StoreItem implements the game's ILeanplumVariables contract, and each field — enabled flag, contents, SKU, cost, duration, reappearance — has a path to be overridden from Leanplum. StoreItemContent encodes to and from a compact string form (e.g. gp.500, gear.Steel.2, cb.<bundle>.1) precisely so a whole reward payload can round-trip through a remote-config value. The result: pricing and bundles can be A/B tested and retuned from a dashboard, without a build.

Underneath, a PurchaseController ScriptableObject owns the runtime: it initializes the Tilting Point SDK purchaser and Unity IAP from the item list, maps each SKU to a single CrossPlatformProduct with matching iOS/Android ids, and raises one OnPurchaseProduct event carrying a result code. When the store is built for display, it filters items by availability, scene, and purchase limits, drops empty categories, and sorts everything by the Order weight — so the UI never has to know the rules.

The shape of it

flowchart TB
  CFG[StoreConfig ScriptableObject
categories + items · designer-authored] LP[Leanplum remote overrides
cost · contents · SKU · timers] CFG --> PC LP -.override.-> PC PC[PurchaseController
init IAP · filter by availability / scene / limits · sort by Order] PC --> UI[Store UI] PC --> POP[Contextual popups
starter · resupply · featured deal] UI --> BUY[Purchase] POP --> BUY BUY --> IAP[TP SDK Purchaser · Unity IAP
one SKU → iOS + Android] IAP --> REW[Reward delivery
currency · gear · city bundle]

Why it's built this way

Takeaway — a shipped game's economy is a live system. Treating the store as a data model a designer authors and a server retunes — rather than hand-written screens — is what lets a small team run live-ops on it without an app update in the loop. It's the pattern engines and platforms would standardize later; here it was built in-house because there was nothing off the shelf to reach for.
Unity · C#ScriptableObjectsIAP · Unity PurchasingLeanplum live-opsGame economy