Trading System is "invalid"

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!

A trading system exactly like the game 60 seconds. Players all share an inventory which is what is in the bunker.

  1. What is the issue? Include screenshots / videos if possible!

While the UI and randomized trades work fine, when I try to submit a trade, it gives me an error. Not sure why as there’s nothing in the output for me.

First, I’ll send my client-side script, then my server script.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TradeEvent = ReplicatedStorage:WaitForChild("TradeEvent")

local Player = game.Players.LocalPlayer
local PlayerGui = Player:WaitForChild("PlayerGui")

local ItemImages = ReplicatedStorage:WaitForChild("ItemImages")

-- GUI references
local TradeGui = PlayerGui:WaitForChild("Trading")
local Offer1Image = TradeGui.ImageLabel:WaitForChild("Offer1")
local Offer2Image = TradeGui.ImageLabel:WaitForChild("Offer2")
local Deal1Button = TradeGui.ImageLabel:WaitForChild("Deal1")
local Deal2Button = TradeGui.ImageLabel:WaitForChild("Deal2")
local StatusLabel = TradeGui.ImageLabel:WaitForChild("StatusLabel")

-- Current trades
local currentTradeIndex = 1
local trades = {
	{ offer1 = {item = "Food", amount = 2}, offer2 = {item = "Water", amount = 1} },
	{ offer1 = {item = "Water", amount = 2}, offer2 = {item = "Food", amount = 1} },
	{ offer1 = {item = "Axe", amount = 1}, offer2 = {item = "Food", amount = 3} },
	{ offer1 = {item = "Radio", amount = 1}, offer2 = {item = "Water", amount = 3} },
	{ offer1 = {item = "Gun", amount = 1}, offer2 = {item = "Food", amount = 4} }
}

local function getImageForItem(itemName)
	local value = ItemImages:FindFirstChild(itemName)
	if value and value:IsA("StringValue") then
		return value.Value
	end
	return ""
end


local function showTrade(index)
	local trade = trades[index]
	if not trade then return end
	local offer1 = trade.offer1
	local offer2 = trade.offer2

	Offer1Image.Image = getImageForItem(offer1.item)
	Offer2Image.Image = getImageForItem(offer2.item)
	
	Deal1Button.Image = getImageForItem(trade.offer2.item)
	Deal2Button.Image = getImageForItem(trade.offer1.item)
end

-- Handle trade requests
Deal1Button.MouseButton1Click:Connect(function()
	TradeEvent:FireServer("RequestTrade", currentTradeIndex, 1)
end)

Deal2Button.MouseButton1Click:Connect(function()
	TradeEvent:FireServer("RequestTrade", currentTradeIndex, 2)
end)

-- Handle server result
TradeEvent.OnClientEvent:Connect(function(success, message)
	if success then
		StatusLabel.Text = "Trade completed!"
	else
		StatusLabel.Text = "Trade failed: " .. (message or "Not enough items.")
	end

	task.delay(3, function()
		StatusLabel.Text = ""
	end)
end)

local currentTradeIndex = 1

local Trader = workspace:WaitForChild("Trader")
local Prompt = Trader:WaitForChild("ProximityPrompt")

local function openTradeGui()
	TradeGui.Enabled = true
	currentTradeIndex = math.random(1, #trades)
	showTrade(currentTradeIndex)
end

Prompt.Triggered:Connect(function()
	openTradeGui()
end)


-- Initialize GUI with first trade
showTrade(currentTradeIndex)

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")

local TradeEvent = ReplicatedStorage:FindFirstChild("TradeEvent") or Instance.new("RemoteEvent")
TradeEvent.Name = "TradeEvent"
TradeEvent.Parent = ReplicatedStorage

local ItemConfig = require(ReplicatedStorage:WaitForChild("ItemConfig"))
local itemCategories = ItemConfig.itemCategories

local trades = {
	{ offer1 = {item = "Food", amount = 2}, offer2 = {item = "Water", amount = 1} },
	{ offer1 = {item = "Water", amount = 2}, offer2 = {item = "Food", amount = 1} },
	{ offer1 = {item = "Axe", amount = 1}, offer2 = {item = "Food", amount = 3} },
	{ offer1 = {item = "Radio", amount = 1}, offer2 = {item = "Water", amount = 3} },
	{ offer1 = {item = "Gun", amount = 1}, offer2 = {item = "Food", amount = 4} }
}

-- Utility Functions

local function getItemCount(itemName)
	local category = itemCategories[itemName]
	if not category then return 0 end

	local slotsFolder = workspace:FindFirstChild("Bunker")
		and workspace.Bunker:FindFirstChild("Slots")
		and workspace.Bunker.Slots:FindFirstChild(category)
	if not slotsFolder then return 0 end

	local count = 0
	for _, slot in ipairs(slotsFolder:GetChildren()) do
		for _, item in ipairs(slot:GetChildren()) do
			if item.Name == itemName then
				count += 1
			end
		end
	end
	return count
end

local function removeItems(itemName, amount)
	local category = itemCategories[itemName]
	if not category then return end

	local slotsFolder = workspace.Bunker.Slots:FindFirstChild(category)
	if not slotsFolder then return end

	local remaining = amount
	for _, slot in ipairs(slotsFolder:GetChildren()) do
		for _, item in ipairs(slot:GetChildren()) do
			if item.Name == itemName and remaining > 0 then
				item:Destroy()
				remaining -= 1
				if remaining <= 0 then return end
			end
		end
	end
end

local function findFreeSlot(category)
	local folder = workspace.Bunker.Slots:FindFirstChild(category)
	if not folder then return nil end
	for _, slot in ipairs(folder:GetChildren()) do
		if #slot:GetChildren() == 0 then
			return slot
		end
	end
	return nil
end

local function addItemToBunker(itemName)
	local category = itemCategories[itemName]
	if not category then warn("No category for item:", itemName) return end
	local slot = findFreeSlot(category)
	if not slot then warn("No free slot in category:", category) return end

	local model = ServerStorage:FindFirstChild(itemName)
	if model then
		local clone = model:Clone()
		clone:SetPrimaryPartCFrame(slot.CFrame)
		clone.Parent = slot
	else
		warn("Missing model for item:", itemName)
	end
end

local function tradeEquals(a, b)
	return a.offer1.item == b.offer1.item and
		a.offer1.amount == b.offer1.amount and
		a.offer2.item == b.offer2.item and
		a.offer2.amount == b.offer2.amount
end


TradeEvent.OnServerEvent:Connect(function(player, action, trade, dealNumber)
	if action ~= "RequestTrade" or typeof(trade) ~= "table" or typeof(dealNumber) ~= "number" then
		TradeEvent:FireClient(player, false, "Invalid request format.")
		return
	end

	-- Make sure the trade matches one of the predefined trades exactly
	local validTrade
	for _, t in ipairs(trades) do
		if tradeEquals(t, trade) then
			validTrade = t
			break
		end
	end

	if not validTrade then
		TradeEvent:FireClient(player, false, "Trade offer is invalid.")
		return
	end

	local success = false
	if dealNumber == 1 then
		if getItemCount(trade.offer2.item) >= trade.offer2.amount then
			removeItems(trade.offer2.item, trade.offer2.amount)
			for _ = 1, trade.offer1.amount do
				addItemToBunker(trade.offer1.item)
			end
			success = true
		else
			TradeEvent:FireClient(player, false, "Not enough " .. trade.offer2.item)
			return
		end
	elseif dealNumber == 2 then
		if getItemCount(trade.offer1.item) >= trade.offer1.amount then
			removeItems(trade.offer1.item, trade.offer1.amount)
			for _ = 1, trade.offer2.amount do
				addItemToBunker(trade.offer2.item)
			end
			success = true
		else
			TradeEvent:FireClient(player, false, "Not enough " .. trade.offer1.item)
			return
		end
	end

	TradeEvent:FireClient(player, success)
end)

I think it would help if you try to pinpoint the issue for us. Add debug prints on every step, test on 2 clients on studio, and see where it takes you.