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.
-- 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
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.
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".
| Check | Answer when it fails |
|---|---|
| The text is a string of at most 30 characters | Invalid code |
| The player's used codes have loaded | Your data is still loading |
| Fewer than 6 tries in the last minute | Too many tries, wait a minute |
| The code exists (spaces removed, capitals ignored) | Invalid code |
| It has not expired | This code has expired |
| This player has not used it | You already used this code |
| The use saved | Could not save, try again later |
Step 1: The codes server
- Publish your place, then in Game Settings, Security, turn on Enable Studio Access to API Services, so DataStores work in Studio.
- Insert a 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.
-- 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
- Press Play, press Codes, type
releaseand press Redeem: "Code redeemed! +100 coins" and Coins goes up. - Press Redeem again: "You already used this code".
- Stop and press Play again, then try
release: still "already used", because it was saved. - Type a wrong code 7 times fast: "Too many tries".
- To test expiry, set a code's
expiresto 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.
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.
- 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
- Roblox Creator Docs: Data stores. enabling Studio access, GetAsync and UpdateAsync
- Roblox Creator Docs: GlobalDataStore:UpdateAsync. returning nil cancels the write
- Roblox Creator Docs: Remote events and functions. RemoteFunction and InvokeServer
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.