§ How to · with Picoo
How to make a Roblox shop system (with AI)
By Sametcan Tasgiran, Founder & Developer·Published ·Updated
A Roblox shop needs a way to open it, a window that lists what is for sale, a purchase the server checks, and an inventory that survives a rejoin. Ask Picoo for a shop and it applies its persistent_shop recipe, which builds all four: a vendor Part with a ProximityPrompt, a shop window, a server-side price check against leaderstats Coins, and an inventory saved to DataStore.
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.
Vendor with a ProximityPrompt
An anchored 2 × 5 × 2 stud Part named Merchant, centred at the vendor position (8, 2.5, 0 unless you give one) in a Workspace.Vendors folder, carries a ProximityPrompt called ShopPrompt. The server gives the Part a ShopName attribute, and when a player triggers the prompt, their client opens the shop window.
Server-side price check
The catalog and every price live in the server Script. The client sends only an item id through the PurchaseItem RemoteFunction. The server checks that the id is a string and a known item, compares its own price with the player's leaderstats Coins, and deducts the Coins itself.
1-second purchase cooldown
Each player can buy once per second. A faster request is turned away with the reason cooldown, and the window shows 'Wait a moment'.
Inventory saved to DataStore
Owned counts are stored per player under u_<UserId> in the ShopData_v1_inventory DataStore. The inventory loads on join and saves after every purchase, on leave and on server shutdown, with up to 3 attempts per DataStore call. If the load still fails, nothing is saved for that player, and on a live server their purchases are refused until they rejoin, so an outage cannot overwrite the saved inventory with an empty one; in a Studio play test purchases work but are not saved.
Shop window
A dark ScreenGui panel lists each item with its name, owned count, description and a buy button showing the price. A status line reports the result, such as 'Need 100 coins (have 40)'. The X button or Escape closes it.
Purchase feedback
A successful buy floats the item's name above the buyer, gives the buyer a small camera shake, and plays a chime and a particle burst at the buyer's position, which nearby players see and hear too.
Files Picoo ships for this prompt
8 files · 1259 lines · one recipe call
ServerScriptService/ShopServer.lua
Server Script: the item catalog and prices, the purchase check, the per-player cooldown, and loading and saving each player's inventory.
226 lines
StarterPlayer/StarterPlayerScripts/ShopClient.lua
LocalScript: builds the shop window, opens it when the vendor's prompt is triggered, and sends buy requests.
168 lines
ReplicatedStorage/Remotes/PurchaseItem
RemoteFunction: the client sends an item id, the server returns the result.
instance
ReplicatedStorage/Remotes/GetInventory
RemoteFunction: returns the catalog and the player's owned counts when the shop opens.
instance
Workspace/Vendors/Merchant
The vendor Part: anchored, 2 × 5 × 2 studs, centred at the vendor position. Move it wherever the shop should stand.
instance
Workspace/Vendors/Merchant/ShopPrompt
ProximityPrompt that opens the shop.
instance
ReplicatedStorage/PicooFeedback.lua
Shared game-feel module. The shop calls it for the purchase popup, chime and particles.
852 lines
StarterPlayer/StarterPlayerScripts/PicooFeedbackClient.lua
Loads the client half of PicooFeedback for every player.
13 lines
Sample output: ServerScriptService/ShopServer.lua
--!strict
-- Generated by Picoo recipe: persistent_shop
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
-- (excerpt) opening the DataStore, the catalog, then the purchase handler:
-- GetDataStore itself errors in a place that was never published, and can in
-- Studio without API access. Called bare up here, that one error stopped this
-- whole script before anyone joined. Now a failure only leaves the store nil:
-- every load fails at once, so a Studio play test runs on in memory and saves
-- nothing, and a save (only ever after a load that succeeded) always has it.
local storeOk, storeResult = pcall(function()
return DataStoreService:GetDataStore("ShopData_v1_inventory")
end)
local INVENTORY_STORE: DataStore? = if storeOk then storeResult else nil
local INVENTORY_STORE_ERROR: string? = if storeOk then nil else tostring(storeResult)
local CURRENCY_FIELD = "Coins"
local PURCHASE_COOLDOWN = 1.0 -- seconds between purchases per player
-- Catalog (authoritative — server source of truth, client can't override prices)
local ITEMS = {
["sword"] = { name = "Iron Sword", price = 100, description = "Sharp and reliable" },
["shield"] = { name = "Wooden Shield", price = 50, description = "Basic defense" },
["potion"] = { name = "Health Potion", price = 25, description = "Restore 50 HP" },
}
PurchaseItem.OnServerInvoke = function(player: Player, itemIdArg: any)
-- Server validates EVERY arg
if type(itemIdArg) ~= "string" then return { ok = false, reason = "invalid_args" } end
local item = ITEMS[itemIdArg]
if not item then return { ok = false, reason = "unknown_item" } end
-- Inventory not loaded (load still running, or failed on a live server): the purchase
-- could not be saved, so don't take coins. A Studio play test shops on, unsaved.
if not loaded[player.UserId] and not playTest[player.UserId] then return { ok = false, reason = "not_loaded" } end
-- Per-player purchase cooldown (anti-spam)
local now = tick()
if lastPurchase[player.UserId] and (now - lastPurchase[player.UserId]) < PURCHASE_COOLDOWN then
return { ok = false, reason = "cooldown" }
end
lastPurchase[player.UserId] = now
-- Currency check (server price authoritative, not client-provided)
local currency = ensureLeaderstats(player)
if not currency then return { ok = false, reason = "no_leaderstats" } end
if currency.Value < item.price then
return { ok = false, reason = "insufficient_funds", needed = item.price, have = currency.Value }
end
-- Apply purchase
currency.Value = currency.Value - item.price
local inv = inventoryCache[player.UserId] or {}
inventoryCache[player.UserId] = inv
inv[itemIdArg] = (inv[itemIdArg] or 0) + 1
saveInventory(player)
-- (excerpt) PicooFeedback purchase popup, chime and particles, then:
return { ok = true, item_id = itemIdArg, new_count = inv[itemIdArg], remaining_currency = currency.Value }
endBuilding a Roblox shop system
A shop is where bad server code costs you your economy. If the client says what an item costs, or can change its own Coins, an exploiter can buy everything for nothing. Picoo builds shops from a fixed recipe, persistent_shop, so the price check is on the server every time.
The catalog lives in the server Script: an id, a name, a price and a description for each item. The client never sends a price. When a player presses Buy, ShopClient calls the PurchaseItem RemoteFunction with the item id alone. ShopServer checks that the argument is a string and a known item, refuses the purchase if the player's inventory has not loaded (in a Studio play test whose load failed it sells, but saves nothing), turns away a second purchase within one second, reads the player's leaderstats Coins, and returns the amount needed if the balance is short. Only then does it subtract the price, add one to the item's count and save.
The inventory has its own DataStore, ShopData_v1_inventory, keyed by u_ and the player's UserId. It loads when the player joins and saves after each purchase, when the player leaves and when the server shuts down. At shutdown it saves every player at once and waits until all the saves finish, at most 25 seconds, so on a big server nobody late in the list is left unsaved. Every DataStore call gets up to 3 attempts, 2 and then 4 seconds apart, and a call that still fails is logged with warn. If that call is the load, ShopServer saves nothing for that player until they rejoin, so an outage cannot overwrite their saved inventory with an empty one.
The storefront is an anchored 2 × 5 × 2 stud Part named Merchant, standing at the vendor position, with a ProximityPrompt. The server tags the Part with a ShopName attribute, and the client opens the window only for prompts on a tagged Part. The window lists each item with its owned count and a price button, says why a purchase failed, and closes with the X button or Escape. A successful buy also calls PicooFeedback, which floats the item name over the buyer, shakes their camera slightly, and plays a chime and a particle burst at their position.
The shop spends the Coins value it finds in leaderstats. Earning Coins, saving the balance, Robux sales and turning a purchase into a Tool in the Backpack are the natural follow-up prompts.
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
What does Picoo build when I ask for a shop?+
Its persistent_shop recipe: a server Script (ShopServer), a LocalScript that draws the shop window (ShopClient), two RemoteFunctions (PurchaseItem and GetInventory), a vendor Part named Merchant with a ProximityPrompt, and the shared PicooFeedback module for the purchase effects. Press Play, walk up to the Merchant and trigger the prompt to open the shop.
Does the shop check prices on the server?+
Yes. The catalog and prices live in ShopServer, and the client sends only the item id. The server checks that the id is a string and a real item, applies the 1-second per-player cooldown, compares the server's price with leaderstats Coins, and deducts the Coins itself. GetInventory sends the catalog to the client only so it can draw the window.
Where do players get Coins?+
The shop spends a Coins IntValue in the player's leaderstats and creates one at 0 if the player has none. What it saves is the inventory. Ways to earn Coins, and saving the Coins balance between sessions, are follow-up prompts.
Can I change the items, prices and currency?+
Yes. Describe them when you ask. The recipe takes a list of items (id, name, price, description), a vendor name and position, a DataStore name and the leaderstats value to spend, Coins by default. Without a list you get an Iron Sword for 100, a Wooden Shield for 50 and a Health Potion for 25. Robux sales through developer products and ProcessReceipt, game pass checks and limited stock are follow-up prompts.
What does a player get when they buy something?+
The server adds 1 to that item's count in the player's inventory and saves it, and the window shows the new 'owned' count. Turning an owned item into a Tool in the Backpack, a pet or a stat boost is up to your game; ask for it in a follow-up prompt.
What if my game already has its own economy?+
The recipe installs a complete shop, so Picoo applies it only to a place that does not already have your own scripts and systems in it. In a game you have already built, Picoo reads the scripts that are there and adds the shop to them instead.
Related Picoo prompts
Roblox pet system
6 files · 1,165 lines · one recipe call. Eggs, rarity, trading and an inventory UI are follow-up prompts that call PetServer.grant and the EquipPet remote.
Roblox tycoon
19 files · 5,513 lines, 2,268 of them the vendored ProfileStore module · one recipe call. Plot1 is built in Edit mode; plots 2-6 are copies of it made when the server starts.
How to use Roblox DataStore the right way
1 file · 155 lines · one recipe call. Saves one number per player; name the stat, its starting value and the autosave interval in the prompt.