IntermediateShopRemoteFunctionAnti-exploitInventoryServer

In-Game Shop with Server Validation

A bulletproof shop. Items live in a config module, the client requests purchases via a RemoteFunction, and the server is the only thing that touches coins or inventory. Exploiters can't fake purchases.

2 files124 lines of Luau
The Prompt

Make me a shop system. Players spend coins to buy items, the items go into their inventory. The whole flow needs to be exploit-proof — no client should be able to fake having coins or give themselves items.

Paste this — or any variation — into StudByStud and you’ll get the code below in seconds.

How it works

  • All shop items are defined in a single ShopCatalog ModuleScript shared via ReplicatedStorage. Adding an item is one entry in a table.
  • The client UI calls a RemoteFunction with the itemId it wants to buy. The server is the only place that ever reads price or modifies inventory — the client is never trusted.
  • Before doing anything, the server validates the itemId exists, the player has enough coins, and they don't already own a unique item.
  • If validation passes, the server deducts coins, adds the item to the player's server-side inventory, and returns success. If anything fails, it returns a structured error code.
  • The client receives the result and updates the UI accordingly. Because all state lives on the server, exploiters can spam the RemoteFunction all day without ever stealing items.

The generated code

2 files. Each one is labeled with its Roblox Studio path.

File 1 of 2ModuleScript
ReplicatedStorage/ShopCatalog.lualuau
--!strict
-- ShopCatalog.lua (ReplicatedStorage)

export type ShopItem = {
	id: string,
	name: string,
	description: string,
	price: number,
	unique: boolean, -- if true, player can only own one
}

local Catalog: { [string]: ShopItem } = {
	wood_sword = {
		id = "wood_sword",
		name = "Wooden Sword",
		description = "Better than nothing.",
		price = 50,
		unique = true,
	},
	iron_sword = {
		id = "iron_sword",
		name = "Iron Sword",
		description = "A reliable blade.",
		price = 250,
		unique = true,
	},
	health_potion = {
		id = "health_potion",
		name = "Health Potion",
		description = "Heals 50 HP.",
		price = 25,
		unique = false,
	},
}

return Catalog

The full shop catalog. Designers can add or rebalance items here without touching any logic.

File 2 of 2Script
ServerScriptService/ShopService.lualuau
--!strict
-- ShopService.lua
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Catalog = require(ReplicatedStorage:WaitForChild("ShopCatalog"))

-- Server-only inventory store. Never replicate this directly to clients —
-- send a sanitized snapshot via RemoteEvent if the UI needs it.
local inventories: { [Player]: { [string]: number } } = {}

Players.PlayerAdded:Connect(function(player)
	inventories[player] = {}
end)

Players.PlayerRemoving:Connect(function(player)
	inventories[player] = nil
end)

local PurchaseRemote = Instance.new("RemoteFunction")
PurchaseRemote.Name = "PurchaseRemote"
PurchaseRemote.Parent = ReplicatedStorage

type PurchaseResult = {
	ok: boolean,
	error: string?,
	newBalance: number?,
}

local function getCoins(player: Player): IntValue?
	local leaderstats = player:FindFirstChild("leaderstats")
	if not leaderstats then return nil end
	local coins = leaderstats:FindFirstChild("Coins")
	if coins and coins:IsA("IntValue") then
		return coins
	end
	return nil
end

PurchaseRemote.OnServerInvoke = function(player: Player, itemId: any): PurchaseResult
	-- 1. Validate input shape (the client could send anything)
	if type(itemId) ~= "string" then
		return { ok = false, error = "INVALID_INPUT" }
	end

	-- 2. Validate item exists
	local item = Catalog[itemId]
	if not item then
		return { ok = false, error = "UNKNOWN_ITEM" }
	end

	-- 3. Validate uniqueness rule
	local inv = inventories[player]
	if not inv then
		return { ok = false, error = "NO_INVENTORY" }
	end
	if item.unique and (inv[itemId] or 0) > 0 then
		return { ok = false, error = "ALREADY_OWNED" }
	end

	-- 4. Validate balance
	local coins = getCoins(player)
	if not coins then
		return { ok = false, error = "NO_BALANCE" }
	end
	if coins.Value < item.price then
		return { ok = false, error = "INSUFFICIENT_COINS" }
	end

	-- 5. All checks passed — commit
	coins.Value -= item.price
	inv[itemId] = (inv[itemId] or 0) + 1

	return { ok = true, newBalance = coins.Value }
end

Handles every purchase. Validates the player has the coins, deducts them atomically, and adds the item to their server inventory.

What you’ll learn

RemoteFunction for request-response patternsServer-only inventory state on the playerDefensive validation of every client argumentReturning structured success / error results to the clientConfig-driven shop catalog (no script edits to add items)

Want this built for your game?

Sign up, paste the prompt above, and StudByStud will generate it — and sync it straight into Roblox Studio. Free tier includes 1M Flash tokens per month.

Build it free