---
name: gamedev-skills-roblox-simulator-economy
description: "Design, balance, and implement the economy of a Roblox simulator, tycoon, clicker, or idle game in Luau: upgrade cost curves, buy max, income per second, rebirth/prestige requirements and multipliers, offline earnings, big-number display (1.5K, 3.4M, 2.0Qa), server-authoritative purchases, repairing tampered or old save data, and odds disclosure for paid random items such as eggs, crates, and spins. Includes a Lune pacing calculator. Use when choosing or checking baseCost/costGrowth/rebirth numb"
license: "Apache-2.0"
metadata:
  decimalai-skill-id: "fde24327-2f21-4256-9b66-76742093b27f"
  decimalai-version-id: "50d7f95c-4e0b-47d7-bc2d-66ac425b404e"
  decimalai-version: "1"
  decimalai-body-sha256: "37151ac732e2e42486222c8b5ad66ef833a40864e22edf2dd211e3aa6fffa537"
  decimalai-name: "gamedev-skills/roblox-simulator-economy"
  decimalai-format: "1"
  decimalai-invocation: "model"
  decimalai-category: "coding"
  decimalai-source: "https://app.decimal.ai/s/gamedev-skills-roblox-simulator-economy@1/SKILL.md"
---
# Roblox simulator economy

Choose the economy's numbers with a pacing table before writing gameplay code, then implement them
so the server owns every amount. Code targets Luau on Roblox's rolling platform APIs; the bundled
scripts are pure Luau and were run with **Lune 0.11.0**, outside Studio.

## When to use

- Picking or reviewing `baseCost`, `costGrowth`, rebirth requirement and multiplier, or offline
  cap and efficiency values for a simulator, tycoon, clicker, or idle game.
- Writing upgrade, buy-max, currency, rebirth, or offline-earnings code in Luau.
- Displaying large numbers, or values above about `1e15` that lose precision.
- Adding eggs, crates, or spins bought with Robux, or with currency that can be bought with Robux.

**When not to use:** DataStore reads, writes, session locking and retries → `roblox-datastores`.
Remote protocol design and rate limiting in depth → `roblox-networking`. Shop and HUD layout →
`roblox-ui` + `game-ui-ux`. A wave-defense economy → `tower-defense`. Engine-neutral save format
questions → `save-systems`.

## Workflow

1. **Inspect.** Find where currency, upgrade, and rebirth values live now and which scripts change
   them. List every RemoteEvent that touches currency and what each one sends.
2. **Write the numbers as one config table**: base income; each upgrade's `baseCost`,
   `costGrowth`, effect, and `maxLevel`; rebirth `baseRequirement`, `requirementGrowth`,
   `multiplierPerRebirth`; offline `maxHours`, `efficiency`. Server and client both read it.
3. **Run the pacing calculator** from the skill directory (paths are relative to it):
   `lune run scripts/balance.luau --print-defaults > economy.json`, edit the file, then
   `lune run scripts/balance.luau economy.json`. Read the `wait` and `vs prev` columns and tune
   until they match the targets below. `--help` lists every field and its allowed range; a
   missing file, invalid JSON, or out-of-range field prints an error and exits 1.
4. **Implement on the server** with the patterns below and
   [references/server-patterns.md](references/server-patterns.md). Copy
   `scripts/economy_math.luau` unchanged into a ModuleScript so the maths you tested is the maths
   that runs.
5. **Verify.** `lune run scripts/test_economy_math.luau` (exits 1 on failure) for the maths; a
   Studio playtest with Server & Clients for purchases, rebirth, leaving and rejoining, and
   invalid remote arguments.

## Formulas

| Quantity | Formula | Notes |
|---|---|---|
| Price of the next level | `floor(baseCost * costGrowth ^ owned)` | 1.07-1.15 for upgrades bought many times; 1.5-3 for rare large ones |
| Cost from level a to b | `baseCost * g^a * (g^(b-a) - 1) / (g - 1)` | Players pay the floored per-level sum, slightly less |
| Buy max | walk real prices until the next exceeds the balance or `maxLevel` | Safer than the log formula with floored prices |
| Rebirth requirement k | `baseRequirement * requirementGrowth ^ k` | |
| Rebirth multiplier | `1 + k * multiplierPerRebirth` | Additive grows slowly; multiplicative (`q ^ k`) grows exponentially |
| Offline income | `floor(min(away, maxHours * 3600) * rate * efficiency)` | Away time of 0 or less (clock went back) pays 0 |

Derivations, the rebirth wait-ratio formula, and a worked example:
[references/formulas.md](references/formulas.md).

## Pacing targets (starting points, not rules)

- **Early upgrades take seconds.** In the default config the first 10 levels take 3-10 s each.
- **Each rebirth should take only a little longer than the last**, for example x1.2-x1.5. The
  calculator's `vs prev` column is that ratio. With an additive multiplier it approaches
  `requirementGrowth` over time, so `requirementGrowth = 3` gives x2.0 rising to x2.7 and rebirth 8
  takes 137 days at the default income. `requirementGrowth = 1.8` gives x1.2 rising to x1.6.
- **Offline earns less than online.** An efficiency of 0.25-0.5 and an 8-12 hour cap keep playing
  worth more than idling without zeroing out players who log off.

## Server-owned purchases

A RemoteEvent carries what the player wants ("buy upgrade `Income`"), never an amount or a price.
The server looks up the price, checks the balance, spends, and grants without yielding in between.

```lua
-- ServerScriptService/Economy.server.luau (excerpt; full version in references/server-patterns.md)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local EconomyMath = require(ReplicatedStorage.Shared.EconomyMath)
local Config = require(ReplicatedStorage.Shared.EconomyConfig)

ReplicatedStorage.Remotes.BuyUpgrade.OnServerEvent:Connect(function(player: Player, upgradeId: unknown)
	local session = sessions[player]
	if not session or type(upgradeId) ~= "string" then return end
	local upgrade = Config.upgrades[upgradeId]
	if not upgrade then return end -- unknown id
	local owned = session.levels[upgradeId] or 0
	if owned >= upgrade.maxLevel then return end
	local price = EconomyMath.levelCost(upgrade.baseCost, upgrade.costGrowth, owned)
	if EconomyMath.trySpend(session.wallet, { [upgrade.currency] = price }) then
		session.levels[upgradeId] = owned + 1
	end
end)
```

- `trySpend` checks every currency in a multi-currency cost before subtracting any, and rejects
  negative or non-finite costs.
- **Rebirth on the server**: compare the server's balance with the requirement, then reset wallet
  and levels and increment the count. Never accept "I rebirthed" or a balance from the client.
- Rate-limit each remote per player with a token bucket (`roblox-networking`).

## Save data and offline time

- **Repair on load; never error.** Use `sanitizeAmount` (non-numbers, NaN, infinity and negatives
  become 0; fractions are floored) and `sanitizeLevel` (also clamps to the current `maxLevel`).
  Drop unknown upgrade ids and return a list of what was repaired, so old versions and edited data
  load cleanly.
- **Never save NaN or infinity.** A division by zero that reaches the wallet should be caught by
  the same sanitizers before saving.
- **Offline time comes from the server clock only**: save `os.time()` on the server when the
  player leaves; on join, `away = os.time() - lastSeen`. A missing or future `lastSeen` counts as
  now, which pays nothing.
- **Precision:** Luau numbers are doubles, exact for integers up to `2^53` (about `9e15`). If
  balances can pass about `1e15`, store a mantissa/exponent pair or a base-10 logarithm instead.

## Big-number display

`EconomyMath.formatNumber(value, decimals?)` uses the suffixes K, M, B, T, Qa, Qi, Sx, Sp, Oc, No,
Dc, Ud, Dd, Td, Qad, Qid, Sxd, Spd, Ocd, Nod, Vg (`1e63`), then scientific notation.

- **The rollover bug:** `999950 / 1000 = 999.95`, which rounds to `"1000.0K"`. Round first; if the
  rounded value reaches 1000, move to the next suffix (`"1.0M"`). The tests cover this case.
- Format on the client for display only; never parse a formatted string back into a number.

## Paid random items (eggs, crates, spins)

Roblox's [paid random items guidelines](https://create.roblox.com/docs/production/monetization/paid-random-items)
apply when a random item is bought with Robux, directly or through in-game currency bought with
Robux. Per that page:

- Show every possible outcome and its actual numerical odds, as percentages that sum to exactly
  100%, before purchase (long decimals may be rounded, with a note that the shown total may not be
  exactly 100%).
- Probability modifiers such as luck boosts and pity systems are covered: explain their effect
  numerically before purchase, and update the shown odds live while a boost is active.
- If the full list does not fit, open it from a control labelled "Info" or "Details" that is
  visible before purchase; an "(i)" icon without a word is not enough.
- `PolicyService:GetPolicyInfoForPlayerAsync()` returns `ArePaidRandomItemsRestricted`; for those
  players, apply one of the treatments the page lists.

Build the odds panel with `EconomyMath.oddsPercent(outcomes)` and roll on the server with
`EconomyMath.pickWeighted(outcomes, rng:NextNumber())` from the **same** list (after applying any
boost), so shown and actual odds cannot drift.

## Pitfalls

- **Rebirth 6 takes weeks** → `requirementGrowth` far above the multiplier's growth; read the
  `vs prev` column and lower `requirementGrowth` or switch to a multiplicative multiplier.
- **Prices outrun income after ~20 levels** → `costGrowth` of 1.5+ on an upgrade meant
  to be bought 50 times (level 50 costs about 637 million times level 0); use 1.07-1.15.
- **Late game breaks after a few rebirths** → multiplicative multiplier (`1.5 ^ k`) without a
  matching requirement curve; keep `requirementGrowth / q` near the ratio you want.
- **Players gain currency they did not earn** → a remote accepts an amount or price, or the check
  happens only on the client; send ids, price on the server.
- **Two purchases pass one balance check** → the handler yields between check and spend; spend
  before any `task.wait`, DataStore call, or other yield.
- **Buy max freezes the server** → uncapped loop or a price of 0; pass `maxLevel`, keep
  `baseCost >= 1` (`maxAffordable` errors instead of looping forever).
- **Offline pays days of income** → client timestamp, no cap, or a corrupted `lastSeen`; use the
  server clock, cap, and treat future times as now.
- **HUD shows "1000.0K"** → `math.floor(x * 10) / 10` before choosing the suffix; use
  `formatNumber`.
- **A save fails or loads as garbage** → NaN or infinity reached the wallet; sanitize before save
  and on load.

## Resources

- `scripts/balance.luau` — pacing calculator: per-level price, total, income and wait; per-rebirth
  requirement, multiplier, wait and ratio; offline maximum. Run with `--help` first.
- `scripts/economy_math.luau` — the formulas, `trySpend`, sanitizers, odds and `formatNumber` as
  pure Luau; runs in Lune and in a Roblox ModuleScript.
- `scripts/test_economy_math.luau` — tests for every function (`lune run`, exits 1 on failure).
- [references/formulas.md](references/formulas.md) — derivations, rebirth ratio, worked example.
- [references/server-patterns.md](references/server-patterns.md) — config layout, income tick,
  buy max, rebirth, load repair with offline income, and paid random item rolls.

## Related skills

- `roblox-luau` — services, ModuleScripts, and the client/server split.
- `roblox-networking` — remote validation and token-bucket rate limits.
- `roblox-datastores` — loading and saving the session this skill repairs.
- `roblox-ui` — shop, upgrade, and odds panels.
- `roblox-studio-workflow` — Server & Clients playtests.

## Primary references

- `https://create.roblox.com/docs/production/monetization/paid-random-items`
- `https://create.roblox.com/docs/scripting/events/remote`
- `https://create.roblox.com/docs/scripting/security/client-server-boundary`
- `https://create.roblox.com/docs/reference/engine/classes/PolicyService`
- `https://create.roblox.com/docs/reference/engine/datatypes/Random`
- `https://lune-org.github.io/docs`
