---
name: 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 numbers, writing upgrade/currency/rebirth/offline code, or when
progress stalls, a rebirth takes far longer than the last, numbers display as 1000K, offline
earnings pay too much, or players gain currency they did not earn.
---
# 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`