🤠 Wild Country Libs
Example

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.