§ How to · with Picoo

How to make a coin system in Roblox

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

Currency is the first thing an exploiter goes for, and the client-side pickup is how they get it.

Coins must be awarded by the server and saved with a DataStore — but pickup can still feel instant if you separate the feedback from the transaction.

Server-awarded pickups

Touch is validated on the server: the coin exists, is close enough, and has not already been taken.

leaderstats integration

Coins appear in the player list and are readable by every other system.

DataStore persistence

Balances survive rejoin, with pcall and retry so a throttle does not wipe progress.

Respawning coins

Collected coins disappear and return after a delay, per-player or globally.

Instant feel

Sound, particle and the coin vanishing play locally the moment you touch it; the balance follows a frame later.

Anti double-collect

Debounce per coin so one touch does not award five times.

Files Picoo ships for this prompt

2 files · 150 lines · ~25s · 1 credit

CoinServer

Touch validation, awarding, respawn timers, DataStore save/load.

110 lines

CoinClient

Local pickup feedback — sound, particles, immediate hide.

40 lines

Sample output: ServerScriptService.CoinServer

-- Touched fires once per touching PART — a character brushing a coin raises it
-- 5-30 times. Without a debounce that is 5-30 coins for one pickup.
local taken: { [BasePart]: boolean } = {}

local function onCoinTouched(coin: BasePart, hit: BasePart)
	if taken[coin] then return end

	local char = hit:FindFirstAncestorOfClass("Model")
	local plr = char and Players:GetPlayerFromCharacter(char)
	if not plr then return end

	-- Sanity: the coin must still be in the world and the player near it.
	-- Cheap, and it blocks "I touched every coin on the map" replays.
	local hrp = char:FindFirstChild("HumanoidRootPart") :: BasePart?
	if not hrp or (hrp.Position - coin.Position).Magnitude > 12 then return end

	taken[coin] = true
	coin.Transparency = 1
	coin.CanTouch = false

	local stats = plr:FindFirstChild("leaderstats")
	local coins = stats and stats:FindFirstChild("Coins") :: IntValue?
	if coins then coins.Value += COIN_VALUE end

	task.delay(RESPAWN_AFTER, function()
		taken[coin] = nil
		coin.Transparency = 0
		coin.CanTouch = true
	end)
end

Building a coin system in Roblox

Coins are the smallest complete economy in a Roblox game, which makes them the best place to learn where trust belongs.

The naive version puts the pickup in a LocalScript: touch the coin, add one to leaderstats, play a sound. It looks correct on your screen and nowhere else — leaderstats written on the client do not replicate, so the number resets the moment the character respawns. Routing it through a RemoteEvent that adds whatever the client sends is worse: now it replicates, and anyone can ask for a million.

So the server owns the transaction. It checks that the coin has not already been taken, that the player is actually near it, and only then adds to leaderstats. Both checks are cheap and both block real exploits — the distance check in particular stops a replayed touch event from collecting the whole map.

Debouncing matters more here than almost anywhere else. Touched fires once per touching part, so a character brushing a coin can raise it a dozen times in three frames. Without a per-coin guard, one pickup pays out a dozen times, and it will look like a mysterious balance bug rather than what it is.

Feel is the last piece, and it is what separates a game that plays well from one that is merely correct. Play the sound, the particle and the coin's disappearance locally the instant the player touches it. The balance updates a frame or two later when the server agrees. The player perceives an instant pickup; the number they can spend is still the one the server decided.

See more on the Luau generator, the game builder, or browse the full blog.

Frequently asked

Can I award coins from a LocalScript?+

You can, and the value will not replicate — leaderstats changed on the client are visible only to that client and vanish on respawn. Worse, if you route it through a RemoteEvent that simply adds what the client asks for, you have built a free-money button. The server decides.

Why did my player get 10 coins from one coin?+

Touched fires once per touching part, so a character walking over a coin raises it many times in a few frames. Debounce per coin (or per coin-player pair) before awarding anything.

How do I save coins between sessions?+

A DataStore keyed by UserId, written on PlayerRemoving and on BindToClose, always inside pcall with a retry. A DataStore call that throws on shutdown and is not caught is how progress silently disappears.

Should coins respawn?+

Depends on the loop. A collectathon usually respawns them on a timer so the map stays alive; a progression game usually does not, and then you need to persist WHICH coins were taken, not just the total.

How do I make pickup feel instant without trusting the client?+

Play the effect locally on touch — sound, particle, hide the coin — and let the server confirm the balance a moment later. The player experiences an instant pickup; the number they see is still the server's.

Related Picoo prompts