Loading the skills library…

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.
npx skills add gamedev-skills/awesome-gamedev-agent-skills --skill roblox-simulator-economyInstalls from the source repository, including any supporting files.
Downloads contain the instructions shown here. Use the install command for the complete skill.
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.
baseCost, costGrowth, rebirth requirement and multiplier, or offline
cap and efficiency values for a simulator, tycoon, clicker, or idle game.1e15 that lose precision.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.
baseCost,
costGrowth, effect, and maxLevel; rebirth baseRequirement, requirementGrowth,
multiplierPerRebirth; offline maxHours, efficiency. Server and client both read 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.scripts/economy_math.luaulune 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.| 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.
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.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.
-- 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.roblox-networking).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.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.2^53 (about 9e15). If
balances can pass about 1e15, store a mantissa/exponent pair or a base-10 logarithm instead.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.
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.Roblox's paid random items guidelines apply when a random item is bought with Robux, directly or through in-game currency bought with Robux. Per that page:
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.
requirementGrowth far above the multiplier's growth; read the
vs prev column and lower requirementGrowth or switch to a multiplicative multiplier.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.1.5 ^ k) without a
matching requirement curve; keep requirementGrowth / q near the ratio you want.task.wait, DataStore call, or other yield.maxLevel, keep
baseCost >= 1 (maxAffordable errors instead of looping forever).lastSeen; use the
server clock, cap, and treat future times as now.math.floor(x * 10) / 10 before choosing the suffix; use
formatNumber.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).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.https://create.roblox.com/docs/production/monetization/paid-random-itemshttps://create.roblox.com/docs/scripting/events/remotehttps://create.roblox.com/docs/scripting/security/client-server-boundaryhttps://create.roblox.com/docs/reference/engine/classes/PolicyServicehttps://create.roblox.com/docs/reference/engine/datatypes/Randomhttps://lune-org.github.io/docs---
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`