Tutorial · Rewards

How to make a codes system in Roblox Studio

Codes are the cheapest marketing a Roblox game has: post one on your Discord or in the game description, and players come back to type it in. A code system needs three things to be worth it: the codes live only on the server, each player can use each code once, and that "once" is saved, so leaving and rejoining does not work. This guide builds all three, plus expiry dates and a simple Codes box.

Updated 23 October 20266 min readIntermediateLuau

In short

Keep the codes in a table in a Script in ServerScriptService, never in anything the client can read. A RedeemCode RemoteFunction takes the typed text, upper-cases it, looks it up, checks the expiry and whether this player used it, then saves the use with UpdateAsync before giving the reward. The client shows the answer the server returns.

  • Save first, then reward. If the save fails, the code is not given, so it cannot be claimed again after a rejoin.
  • Limit tries per minute, so nobody can guess codes with a script.
  • Expiry uses os.time(), a Unix time in UTC, so a code ends at the same moment on every server.

Get the script

Redeem codes server script with saving

A Script in ServerScriptService that checks typed codes, expiry and one use per player, saves used codes in a DataStore and gives coins.

Script in ServerScriptService named CodeService
-- Script in ServerScriptService named CodeService.
-- Players type a code; the server checks it, saves that it was used and gives the reward.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

type Code = { coins: number, expires: number? } -- expires: a Unix time, or nil for never
-- Codes are not case sensitive: write them here in capitals
local CODES: { [string]: Code } = {
	RELEASE = { coins = 100 },
	THANKYOU = { coins = 250, expires = 1798761600 }, -- 1 January 2027, 00:00 UTC
}
local TRIES_PER_MINUTE = 6 -- stops players from guessing codes quickly

local store = DataStoreService:GetDataStore("RedeemedCodes_v1")
local redeemFunction = Instance.new("RemoteFunction")
redeemFunction.Name = "RedeemCode"
redeemFunction.Parent = ReplicatedStorage

local used: { [Player]: { [string]: boolean } } = {} -- loaded on join
local tries: { [Player]: { number } } = {}
local busy: { [Player]: boolean } = {}

local function addCoins(player: Player, amount: number)
	local leaderstats = player:FindFirstChild("leaderstats")
	local coins = leaderstats and leaderstats:FindFirstChild("Coins")
	if coins and coins:IsA("IntValue") then
		coins.Value += amount
	end
end

Players.PlayerAdded:Connect(function(player: Player)
	local ok, data = pcall(function()
		return store:GetAsync(tostring(player.UserId))
	end)
	if ok then
		used[player] = if type(data) == "table" then data else {}
	else
		warn("CodeService: could not load codes for", player.Name, data)
	end
end)

Players.PlayerRemoving:Connect(function(player: Player)
	used[player] = nil
	tries[player] = nil
	busy[player] = nil
end)

local function tooManyTries(player: Player): boolean
	local now = os.clock()
	local recent: { number } = {}
	local previous: { number } = tries[player] or {}
	for _, t in previous do
		if now - t < 60 then
			table.insert(recent, t)
		end
	end
	table.insert(recent, now)
	tries[player] = recent
	return #recent > TRIES_PER_MINUTE
end

redeemFunction.OnServerInvoke = function(player: Player, text: unknown): string
	if typeof(text) ~= "string" or #text > 30 then
		return "Invalid code"
	end
	local list = used[player]
	if not list then
		return "Your data is still loading, try again in a moment"
	end
	if busy[player] then
		return "One moment..."
	end
	if tooManyTries(player) then
		return "Too many tries, wait a minute"
	end
	local name = string.upper((string.gsub(text, "%s", "")))
	local code = CODES[name]
	if not code then
		return "Invalid code"
	end
	if code.expires and os.time() > code.expires then
		return "This code has expired"
	end
	if list[name] then
		return "You already used this code"
	end

	-- Save first, then reward: a code that failed to save is not given out
	busy[player] = true
	local alreadySaved = false
	local ok = pcall(function()
		store:UpdateAsync(tostring(player.UserId), function(old: any)
			alreadySaved = false -- UpdateAsync can run this more than once: only the last run counts
			local saved = if type(old) == "table" then old else {}
			if saved[name] then
				alreadySaved = true -- used on another server since this one loaded
				return nil -- returning nil cancels the write
			end
			saved[name] = true
			return saved
		end)
	end)
	busy[player] = nil
	if not ok then
		return "Could not save, try again later"
	end
	list[name] = true
	if alreadySaved then
		return "You already used this code"
	end
	addCoins(player, code.coins)
	return string.format("Code redeemed! +%d coins", code.coins)
end

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: the server script and the Codes box
ServerScriptService
  CodeService Script new
StarterPlayer
  StarterPlayerScripts
    CodesMenu LocalScript new

Settings you can change

CODESIn the script
Your codes in capitals, with the coins each gives and an optional expiry as a Unix time.
TRIES_PER_MINUTEIn the script
How many codes one player can try in a minute. Default: 6.

Saving needs Enable Studio Access to API Services (Game Settings, Security) to test in Studio, and a published place. Players need a Coins leaderstat, as in the coin guide.

What a cheater can and cannot doCodes exist only on the server, so they cannot be read from the game files. The server limits tries, checks expiry and the saved list, and gives the reward only after the use is saved.

Type-checked with luau-lsp strict against Roblox's API definitions on 23 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add a codes system: a Codes button that opens a box to type a code, a RemoteFunction, and a server Script with a table of codes that give coins and can expire, that allows each player each code once, saves used codes in a DataStore with UpdateAsync before giving the reward, and limits players to 6 tries a minute.

On this page
  1. Get the script
  2. How it works
  3. 1. Server
  4. 2. Codes box
  5. Common errors
  6. With RoCode
  7. Questions
  8. Sources

How a codes system works

The player types a code and presses Redeem. The Codes box calls the RedeemCode RemoteFunction, which waits for the server's answer. The server runs its checks in order and returns a message for the box to show. A RemoteFunction suits this because the player needs an answer: "Code redeemed", "already used" or "expired".

The checks, in order
CheckAnswer when it fails
The text is a string of at most 30 charactersInvalid code
The player's used codes have loadedYour data is still loading
Fewer than 6 tries in the last minuteToo many tries, wait a minute
The code exists (spaces removed, capitals ignored)Invalid code
It has not expiredThis code has expired
This player has not used itYou already used this code
The use savedCould not save, try again later

Step 1: The codes server

  1. Publish your place, then in Game Settings, Security, turn on Enable Studio Access to API Services, so DataStores work in Studio.
  2. Insert a Script in ServerScriptService named CodeService:
Script in ServerScriptService named CodeService
-- Script in ServerScriptService named CodeService.
-- Players type a code; the server checks it, saves that it was used and gives the reward.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

type Code = { coins: number, expires: number? } -- expires: a Unix time, or nil for never
-- Codes are not case sensitive: write them here in capitals
local CODES: { [string]: Code } = {
	RELEASE = { coins = 100 },
	THANKYOU = { coins = 250, expires = 1798761600 }, -- 1 January 2027, 00:00 UTC
}
local TRIES_PER_MINUTE = 6 -- stops players from guessing codes quickly

local store = DataStoreService:GetDataStore("RedeemedCodes_v1")
local redeemFunction = Instance.new("RemoteFunction")
redeemFunction.Name = "RedeemCode"
redeemFunction.Parent = ReplicatedStorage

local used: { [Player]: { [string]: boolean } } = {} -- loaded on join
local tries: { [Player]: { number } } = {}
local busy: { [Player]: boolean } = {}

local function addCoins(player: Player, amount: number)
	local leaderstats = player:FindFirstChild("leaderstats")
	local coins = leaderstats and leaderstats:FindFirstChild("Coins")
	if coins and coins:IsA("IntValue") then
		coins.Value += amount
	end
end

Players.PlayerAdded:Connect(function(player: Player)
	local ok, data = pcall(function()
		return store:GetAsync(tostring(player.UserId))
	end)
	if ok then
		used[player] = if type(data) == "table" then data else {}
	else
		warn("CodeService: could not load codes for", player.Name, data)
	end
end)

Players.PlayerRemoving:Connect(function(player: Player)
	used[player] = nil
	tries[player] = nil
	busy[player] = nil
end)

local function tooManyTries(player: Player): boolean
	local now = os.clock()
	local recent: { number } = {}
	local previous: { number } = tries[player] or {}
	for _, t in previous do
		if now - t < 60 then
			table.insert(recent, t)
		end
	end
	table.insert(recent, now)
	tries[player] = recent
	return #recent > TRIES_PER_MINUTE
end

redeemFunction.OnServerInvoke = function(player: Player, text: unknown): string
	if typeof(text) ~= "string" or #text > 30 then
		return "Invalid code"
	end
	local list = used[player]
	if not list then
		return "Your data is still loading, try again in a moment"
	end
	if busy[player] then
		return "One moment..."
	end
	if tooManyTries(player) then
		return "Too many tries, wait a minute"
	end
	local name = string.upper((string.gsub(text, "%s", "")))
	local code = CODES[name]
	if not code then
		return "Invalid code"
	end
	if code.expires and os.time() > code.expires then
		return "This code has expired"
	end
	if list[name] then
		return "You already used this code"
	end

	-- Save first, then reward: a code that failed to save is not given out
	busy[player] = true
	local alreadySaved = false
	local ok = pcall(function()
		store:UpdateAsync(tostring(player.UserId), function(old: any)
			alreadySaved = false -- UpdateAsync can run this more than once: only the last run counts
			local saved = if type(old) == "table" then old else {}
			if saved[name] then
				alreadySaved = true -- used on another server since this one loaded
				return nil -- returning nil cancels the write
			end
			saved[name] = true
			return saved
		end)
	end)
	busy[player] = nil
	if not ok then
		return "Could not save, try again later"
	end
	list[name] = true
	if alreadySaved then
		return "You already used this code"
	end
	addCoins(player, code.coins)
	return string.format("Code redeemed! +%d coins", code.coins)
end

When a player joins, the script loads their used codes. If the load fails, used[player] stays empty and every redeem answers "still loading", which is safer than letting them claim codes again. UpdateAsync reads the newest saved list, checks the code again, adds it and writes it back, so two servers cannot overwrite each other's changes, and a code used on another server since this one loaded is not paid twice.

To add a code, add a line to CODES. To make one expire, give it expires as a Unix time. 1798761600 is 1 January 2027 at 00:00 UTC; a Unix time converter gives you any other date. Each code change needs a new server, so publish and let old servers close, or shut them down from Creator Hub.

Step 2: The Codes box

Insert a LocalScript in StarterPlayer > StarterPlayerScripts named CodesMenu:

LocalScript in StarterPlayerScripts named CodesMenu
-- LocalScript in StarterPlayerScripts named CodesMenu.
-- A Codes button that opens a box to type a code and redeem it.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local redeemFunction = ReplicatedStorage:WaitForChild("RedeemCode") :: RemoteFunction

local gui = Instance.new("ScreenGui")
gui.Name = "CodesGui"
gui.ResetOnSpawn = false

local function corner(parent: Instance)
	local c = Instance.new("UICorner")
	c.CornerRadius = UDim.new(0, 10)
	c.Parent = parent
end

local openButton = Instance.new("TextButton")
openButton.Text = "Codes"
openButton.Size = UDim2.fromOffset(100, 40)
openButton.Position = UDim2.new(0, 12, 0.5, 0)
openButton.BackgroundColor3 = Color3.fromRGB(110, 80, 220)
openButton.TextColor3 = Color3.new(1, 1, 1)
openButton.Font = Enum.Font.GothamBold
openButton.TextSize = 18
corner(openButton)
openButton.Parent = gui

local panel = Instance.new("Frame")
panel.AnchorPoint = Vector2.new(0.5, 0.5)
panel.Position = UDim2.fromScale(0.5, 0.5)
panel.Size = UDim2.fromOffset(320, 170)
panel.BackgroundColor3 = Color3.fromRGB(30, 26, 40)
panel.Visible = false
corner(panel)
panel.Parent = gui

local box = Instance.new("TextBox")
box.PlaceholderText = "Enter a code"
box.Text = ""
box.ClearTextOnFocus = false
box.Size = UDim2.new(1, -32, 0, 44)
box.Position = UDim2.fromOffset(16, 16)
box.BackgroundColor3 = Color3.fromRGB(50, 44, 66)
box.TextColor3 = Color3.new(1, 1, 1)
box.Font = Enum.Font.Gotham
box.TextSize = 18
corner(box)
box.Parent = panel

local redeemButton = openButton:Clone()
redeemButton.Text = "Redeem"
redeemButton.Size = UDim2.new(1, -32, 0, 40)
redeemButton.Position = UDim2.fromOffset(16, 70)
redeemButton.Parent = panel

local status = Instance.new("TextLabel")
status.Text = ""
status.Size = UDim2.new(1, -32, 0, 40)
status.Position = UDim2.fromOffset(16, 118)
status.BackgroundTransparency = 1
status.TextColor3 = Color3.new(1, 1, 1)
status.Font = Enum.Font.Gotham
status.TextSize = 16
status.TextWrapped = true
status.Parent = panel

gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")

openButton.Activated:Connect(function()
	panel.Visible = not panel.Visible
	status.Text = ""
end)

local waiting = false
redeemButton.Activated:Connect(function()
	if waiting or box.Text == "" then
		return
	end
	waiting = true
	status.Text = "Checking..."
	-- InvokeServer waits for the server's answer; pcall in case the call fails
	local ok, answer = pcall(function()
		return redeemFunction:InvokeServer(box.Text)
	end)
	status.Text = if ok and typeof(answer) == "string" then answer else "Something went wrong, try again"
	waiting = false
end)

InvokeServer waits for the server's answer, so the script wraps it in pcall and blocks a second press while it waits. The box keeps its text when tapped (ClearTextOnFocus = false), so a player can fix a typo.

Test it

  1. Press Play, press Codes, type release and press Redeem: "Code redeemed! +100 coins" and Coins goes up.
  2. Press Redeem again: "You already used this code".
  3. Stop and press Play again, then try release: still "already used", because it was saved.
  4. Type a wrong code 7 times fast: "Too many tries".
  5. To test expiry, set a code's expires to a time in the past.

Common errors and fixes

Watch the Output window on the Server view.

It always says your data is still loading

The DataStore load failed. In Studio, turn on Enable Studio Access to API Services and make sure the place is published. The Output shows the reason after "could not load codes".

Could not save, try again later

The UpdateAsync call failed, usually because of the same Studio setting or a DataStore outage. The player can try again.

A code works again after rejoining

That happens if the reward is given before the save. Use the script as written: it saves first.

A new code says invalid in the live game

Running servers still have the old script. Publish and wait for new servers, or restart them.

Build it with RoCode

If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts in your Explorer.

You type

Add a codes button and box. Codes RELEASE (100 coins) and HALLOWEEN (500 coins, ends 1 November) work once per player, saved, checked on the server.

RoCode does
  • Writes the server codes script with your codes and dates, and the Codes box
  • Reads your place first to use your existing Coins and data saving
  • Is told to compile-check each script with Check Script, and sets undo points around each batch

RoCode can also insert a ready-made store screen with a codes tab from its premium UI kits in one step, in one of five themes, animated, with working server scripts. It cannot turn on Studio Access to API Services for you: do that in Game Settings. How RoCode connects to Studio.

Questions

How do I make codes in my Roblox game?

Keep a table of codes in a server Script, check typed codes through a RemoteFunction, save which codes each player used in a DataStore, and give the reward after the save. Steps 1 and 2 have the scripts.

Where should I put the codes so players cannot find them?

In a Script in ServerScriptService. ReplicatedStorage, StarterGui and LocalScripts are copied to players' devices, so anything there can be read.

How do I make a code expire?

Give it an expiry as a Unix time and compare with os.time() on the server.

Are codes case sensitive?

Not in this script: it removes spaces and upper-cases what the player typed before looking it up.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 23 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.