Trader Shop Example
A complete wc_trader resource — an NPC opens a wc_menu shop, buy/sell prices and stock are validated entirely server-side. See the Creating a Trader Shop guide for the full walkthrough.
fxmanifest.lua
wc_trader/fxmanifest.lua
fx_version 'adamant'
game 'rdr3'
lua54 'yes'
name 'wc_trader'
dependency 'wc_libs'
shared_scripts { '@wc_libs/init.lua' }
client_scripts { 'client.lua' }
server_scripts { 'catalog.lua', 'server.lua' }
catalog.lua (server only)
wc_trader/catalog.lua
Catalog = {
pelt_deer = { label = 'Deer Pelt', buyPrice = 4, sellPrice = 2, stock = 50 },
pelt_bear = { label = 'Bear Pelt', buyPrice = 15, sellPrice = 9, stock = 10 },
ammo_rifle = { label = 'Rifle Ammo', buyPrice = 1, sellPrice = 0, stock = 999 },
}
client.lua
wc_trader/client.lua
local tradePos = vector4(-331.0, 776.0, 119.0, 90.0)
CreateThread(function()
local hash = wc:LoadModel("a_m_m_hillfolk_01")
local traderPed = wc:SpawnPed(hash, tradePos.x, tradePos.y, tradePos.z, tradePos.w, true, true)
wc:PlayScenario(traderPed, 'WORLD_HUMAN_STAND_IMPATIENT')
local prompt = wc:CreatePrompt("Trade [G]")
wc:WatchPrompt(prompt, traderPed, 2.5, function()
TriggerServerEvent('wc_trader:requestCatalog')
end)
end)
RegisterNetEvent('wc_trader:openShop', function(catalog)
local items = { { type = 'label', label = 'For Sale' } }
for sku, entry in pairs(catalog) do
items[#items + 1] = wc.menu.purchase({
id = 'buy_' .. sku,
label = entry.label,
price = { money = entry.buyPrice },
max = 10,
serverEvent = 'wc_trader:buy',
args = { sku = sku },
})
end
wc.menu.open({ id = 'trader_shop', title = 'General Store', themePreset = 'shop', items = items })
end)
server.lua
wc_trader/server.lua
RegisterNetEvent('wc_trader:requestCatalog', function()
-- send only display data — no reason to leak internal stock-management fields
local display = {}
for sku, entry in pairs(Catalog) do
display[sku] = { label = entry.label, buyPrice = entry.buyPrice }
end
TriggerClientEvent('wc_trader:openShop', source, display)
end)
RegisterNetEvent('wc_trader:buy', function(payload)
local src = source
local sku = payload.args and payload.args.sku
local qty = tonumber(payload.value) or 1
local item = Catalog[sku]
if not item or qty < 1 or item.stock < qty then
wc:Notify(src, { variant = 'fail', title = "That's out of stock." })
return
end
local total = item.buyPrice * qty
if wc:GetMoney(src) < total then
wc:Notify(src, { variant = 'fail', title = "Not enough cash." })
return
end
if not wc:CanCarryItem(src, sku, qty) then
wc:Notify(src, { variant = 'fail', title = "You can't carry that much." })
return
end
wc:RemoveMoney(src, total)
local ok = wc:AddItem(src, sku, qty)
if not ok then
wc:AddMoney(src, total) -- refund on failed add
return
end
item.stock = item.stock - qty
wc:Notify(src, { variant = 'avanced', title = ("Bought %dx %s"):format(qty, item.label) })
end)
The client never sees stock or holds any authority over price — requestCatalog strips those fields before sending, and buy recomputes the real total from the server's own Catalog table.