🤠 Wild Country Libs
Guide

Creating A Trader Shop

Buy/sell logic using the inventory bridge, the money bridge, stock checks, and server-side price validation — the pattern behind wc_trader. The menu UI comes from wc_menu; every actual transaction is decided on the server.

The golden rule of trader resources. The client only ever says "I want to buy/sell X." It never gets to say how much that's worth. Prices, stock limits, and item validity all live in a server-side table the client can't touch.
  1. Define your catalog server-side

    This table is the single source of truth for prices. It never ships to the client as anything other than display labels.

    wc_trader/server/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 },
    }
  2. Open the shop menu client-side

    The menu is purely presentational — it shows the catalog and fires a server event per action. See wc_menu for the full item-type reference.

    wc_trader/client/main.lua
    RegisterNetEvent('wc_trader:openShop', function(catalog)
      local items = { { type = 'label', label = 'Buy' } }
    
      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)
  3. Validate the purchase server-side

    Look the SKU up in Catalog, compute the real price from buyPrice * quantity, check the player can afford it, then move money and items together.

    wc_trader/server/main.lua
    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 — inventory rejected the add after we already charged
        return
      end
    
      item.stock = item.stock - qty
      wc:Notify(src, { variant = 'avanced', title = ("Bought %dx %s"):format(qty, item.label) })
    end)
    Order matters. Check afford → check carry → remove money → add item → refund on failure. Never add the item before confirming the money was actually removed, and always have a refund path if the item add fails after payment.
  4. Handle selling the same way, in reverse

    wc_trader/server/main.lua
    RegisterNetEvent('wc_trader:sell', 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 then return end
    
      if not wc:HasItem(src, sku, qty) then
        wc:Notify(src, { variant = 'fail', title = "You don't have that many." })
        return
      end
    
      local ok = wc:RemoveItem(src, sku, qty)
      if not ok then return end
    
      wc:AddMoney(src, item.sellPrice * qty)
      wc:Notify(src, { variant = 'avanced', title = ("Sold %dx %s"):format(qty, item.label) })
    end)

Common mistakes