§ How to · with Picoo
How to make a Roblox currency system (with AI)
By Sametcan Tasgiran, Founder & Developer·Published ·Updated
A Roblox currency system needs five pieces: multiple currency types, safe add and spend operations, server-owned balances, leaderstats integration, and DataStore persistence. Ask Picoo for a currency system and it writes these for your game; the build below is one example.
Example build: 4 files · 165 lines. Cash, Gems and a premium currency.
Multi-currency support
Cash, Gems, and Robux-purchased premium currency. Each has its own balance and UI display, and all are saved together. Configurable to add more (event tokens, season currency).
Checked add and spend
currency:add() and currency:spend() update the balance, the leaderstats value and the save in one call. spend() returns false if the balance is too low, so a balance never goes negative, and on a live server both return false and change nothing while the player's balances have not loaded; in a Studio play test they work but are not saved.
leaderstats integration
Each currency mirrors to leaderstats so it appears on the player list. Automatic — no manual leaderstats setup.
Server-owned balances
Balances live in a server table and every change goes through server functions. The client has no remote that changes a balance, and leaderstats are only a copy the server writes.
DataStore persistence
Balances save on every change, debounced to one write every 30 seconds, and PlayerRemoving saves immediately, so a crash loses at most the last 30 seconds. A load that failed is never saved over: nothing is saved for that player until they rejoin.
Transaction log
Every add/spend logs the reason (quest reward, shop purchase, daily) and timestamp. Stored in a separate DataStore for support disputes and analytics.
Example output from a build like this
4 files in this example. Picoo writes this code for your prompt, so file names and sizes vary.
ServerScriptService/CurrencyManager.lua
Atomic add/spend, leaderstats mirror, transaction log hook
ServerScriptService/CurrencySaveService.lua
Debounced DataStore writes
ServerScriptService/CurrencyLogger.lua
Transaction log writer
StarterGui/CurrencyHUD.lua
Top-corner balance display per currency
Sample output: ServerScriptService/CurrencyManager.lua
--!strict
-- ServerScriptService/CurrencyManager.lua (Picoo · excerpt)
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local SaveService = require(script.Parent.CurrencySaveService)
local Logger = require(script.Parent.CurrencyLogger)
type Currencies = { cash: number, gems: number, premium: number }
local balances: { [Player]: Currencies } = {}
local leaderstatsMap: { [Player]: { [string]: NumberValue } } = {}
-- true ONLY after a load that succeeded. A failed load is never saved over:
-- saving the defaults would replace the player's real balances with them.
local loaded: { [Player]: boolean } = {}
-- A Studio play test of an unpublished place fails every load: it plays on the
-- defaults and is never saved. A live server refuses changes after a failed load.
local playTest: { [Player]: boolean } = {}
local function setupLeaderstats(player: Player, c: Currencies)
local ls = Instance.new("Folder"); ls.Name = "leaderstats"; ls.Parent = player
local map: { [string]: NumberValue } = {}
for currencyName, value in pairs(c) do
local nv = Instance.new("NumberValue")
nv.Name = currencyName:gsub("^%l", string.upper)
nv.Value = value; nv.Parent = ls
map[currencyName] = nv
end
leaderstatsMap[player] = map
end
Players.PlayerAdded:Connect(function(player)
-- ok is false when the DataStore call failed; saved is nil for a new player.
local ok, saved = SaveService:Get(player)
loaded[player] = ok
playTest[player] = not ok and RunService:IsStudio()
if not ok then warn("[Currency] load failed for " .. player.Name .. " (this session will not be saved)") end
saved = saved or { cash = 100, gems = 0, premium = 0 }
balances[player] = saved
setupLeaderstats(player, saved)
end)
local CurrencyManager = {}
-- Returns true only when the balance changed, so a Robux receipt handler can
-- return PurchaseGranted on true and NotProcessedYet otherwise.
function CurrencyManager:add(player: Player, currency: string, amount: number, reason: string): boolean
local b = balances[player]
if not b or not (loaded[player] or playTest[player]) or amount <= 0 then return false end
b[currency] = (b[currency] or 0) + amount
leaderstatsMap[player][currency].Value = b[currency]
if loaded[player] then SaveService:Set(player, b) end -- a play test never saves
Logger:write(player, currency, amount, reason)
return true
end
function CurrencyManager:spend(player: Player, currency: string, amount: number, reason: string): boolean
local b = balances[player]
if not b or not (loaded[player] or playTest[player]) or amount <= 0 then return false end
if (b[currency] or 0) < amount then return false end
b[currency] -= amount
leaderstatsMap[player][currency].Value = b[currency]
if loaded[player] then SaveService:Set(player, b) end -- a play test never saves
Logger:write(player, currency, -amount, reason)
return true
end
return CurrencyManagerBuilding a Roblox currency system
Currency systems are the backbone of any Roblox game with progression. The defining mark of a good currency system: server-authoritative, atomic, multi-type, and instrumented. Get any of those wrong and you have either exploits, lost progress, or no analytics visibility. The example below covers all four.
In this example the currency system is four files. Cash, Gems, and premium currency are set up by default. Adding more is one line in the default balance table, because the engine reads the balance keys dynamically and never needs to know the currency names in advance. That covers new players; a player who already has a save needs the new key merged into it, which is a follow-up prompt.
Checked operations are the safety guarantee. spend(player, 'cash', 50) checks the balance first; if it is too low, it returns false. If there is enough, it deducts and updates leaderstats and the save in the same call, so the balance cannot go negative. On a live server both also refuse while the player's balances have not loaded, because saving after a failed load would replace the stored balances with the defaults; in a Studio play test they work but are not saved.
Server-authoritative balances stop the most common Roblox currency exploit: editing leaderstats on the client. Here balances live in a server-only table, and leaderstats are a copy the server writes. An exploiter who changes leaderstats.Cash on their own client changes only their own screen; the server's balance never moves.
The transaction log is the operational backbone. Every currency change records the player, currency, amount, reason, and timestamp. When a player says they didn't get their daily reward, support reads that player's entries and looks for reason='daily_reward'. The log turns a guessing game into a data question.
See more on the Luau generator, the AI Luau script writer, the 3D model generator and the game builder, read how Picoo compares with ChatGPT, or browse the full blog.
Frequently asked
How do I add a new currency type?+
Add the key to the default balance table (e.g., { cash = 100, gems = 0, premium = 0, eventTokens = 0 }). New players get it with no engine changes, because the leaderstats setup loop reads whatever keys the table has. Players who already have a save do not: their saved table has no eventTokens key, so merging the new defaults into existing saves is a follow-up prompt.
What stops exploits from setting cash to a billion?+
Client code has no access to the balances. Every currency change goes through CurrencyManager:add() or :spend(), which run on the server, and this example has no remote that lets a client pick an amount.
Can I have currency that can't be saved (event-only)?+
Yes. Mark the currency key with an underscore prefix (e.g., '_eventTokens'). In this example SaveService skips keys starting with an underscore, so the balance lives only in memory and is gone on rejoin.
How does the transaction log help?+
Every add and spend records who, what, when, and why ('quest_reward', 'shop_purchase'). When a player files an 'I didn't receive my cash' ticket, support reads that player's log entries instead of guessing.
What about Robux-purchased premium currency?+
MarketplaceService:PromptProductPurchase fires ProcessReceipt. In Picoo's developer product pattern the receipt handler grants through CurrencyManager:add(player, 'premium', amount, 'robux_purchase') only while the player is in the server and their balances loaded, returns PurchaseGranted only after add() returns true, and records each PurchaseId so a receipt is never granted twice. In every other case it returns NotProcessedYet, and Roblox retries the receipt later. The one exception is a Studio play test whose load failed: the test purchase is granted, and nothing is recorded or saved.
What if I only need one saved coin value?+
Ask Picoo to save player coins. That request uses its save recipe, which creates a Coins IntValue in leaderstats, loads it from a DataStore on join, autosaves every 60 seconds and saves again when the server shuts down, and it never saves over a load that failed.
Related Picoo prompts
Roblox shop system
8 files · 1,259 lines, 852 of them the shared PicooFeedback module · one recipe call. Three starter items by default; name your own items and prices in the prompt.
Roblox trading system
Example output: 8 files · 280 lines. Item ownership stays on the server; the model writes the code for your request, so the files vary.
Roblox daily rewards
Example output: 5 files · 170 lines. A 7-day cycle in this example; the model writes the code for your request, so the files and cycle can differ.