Inventory Adapter
The Inventory Adapter is the core interface between a player’s in-game data and the trading system. It acts as the bridge that allows the trade plugin to safely read, lock, move, and commit items across player inventories, regardless of how the underlying data is stored.
The installed adapter lives at ServerScriptService/Traderie/TradeSystemConfig/InventoryAdapter. This file is generated once and preserved when you update the Trade System package.
The adapter must contain these functions:
getSnapshot(userId)getTradableEntries(userId, snapshot)applyDeltas(userId, deltas, opKey)
It can also implement:
canApplyDeltas(userId, deltas)for optional receive or capacity checks.clearAppliedDeltas(userId, opKeys, context)to prune operation keys after the trade system marks them safe to remove.
getSnapshot(userId)
The getSnapshot function retrieves the player's current inventory or profile data. Return whatever snapshot your adapter needs to build tradable entries and apply deltas.
Example
In this example there is an inventory module that returns the player's entire profile.
local Players = game:GetService("Players")
function InventoryAdapter:getSnapshot(userId)
local player = Players:GetPlayerByUserId(userId)
if not player then
return nil
end
return InventoryService.getProfile(player)
end
getTradableEntries(userId, snapshot)
The getTradableEntries function maps your inventory snapshot into entries the trade system can offer, lock, and commit.
Each entry must have a stable entryId. This is the key custom UI code sends back to TradeClient.setOffer.
For kind = "instance" entries, entryId should be the unique item instance id, GUID, or UID, not the shared catalog item id. If a player owns two Golden Eggs, each copy needs a different entryId so the trade system can reserve the exact copy being offered.
For kind = "stack" or kind = "currency" entries, entryId can be the stable stack or currency key, such as egg_gold or coins, because amount identifies how much of that bucket is being offered.
Entries can also include public clientPayload data for UI and reporting, plus private serverPayload data for rollback and commit.
Example
function InventoryAdapter:getTradableEntries(_userId, snapshot)
if not snapshot then
return {}
end
local entries = {}
for _, item in ipairs(snapshot.Data.Inventory) do
entries[item.uid] = {
entryId = item.uid,
kind = "instance",
amount = 1,
clientPayload = {
id = item.id,
name = item.name,
icon = item.icon,
},
serverPayload = {
uid = item.uid,
id = item.id,
},
}
end
return entries
end
TradeEntry shape
{
entryId = "string",
kind = "instance" | "stack" | "currency",
amount = number,
clientPayload = any?,
serverPayload = any?,
}
Use the kind that matches how the player owns the entry:
| Kind | Use for |
|---|---|
instance | A unique item, pet, character, aura, or other object with its own instance id. Send amount = 1. |
stack | A stack of identical items where one inventory entry can hold multiple copies. Send the available stack count as amount. |
currency | A numeric balance such as coins, gems, or another spendable value. Send the available balance as amount. |
clientPayload is sent to clients and Traderie reports. In the example above, clientPayload.id carries the catalog item id while entryId carries the unique inventory item id. serverPayload is stored in the server journal so your adapter can restore or commit the exact item data, but it is never sent to clients or reports.
Custom UI code does not send this full TradeEntry shape back to TradeClient.setOffer. It sends only { entryId = "...", amount = number } offer entries. The trade system resolves those offer entries back against the authoritative TradeEntry data before reserving inventory.
canApplyDeltas(userId, deltas)
The optional canApplyDeltas function can reject a trade before inventory is mutated. Use it for receive limits, inventory capacity, account restrictions, or any other final validation.
function InventoryAdapter:canApplyDeltas(userId, deltas)
if not deltas.add then
return true
end
local profile = self:getSnapshot(userId)
if not profile then
return false, "no_inventory"
end
if #profile.Data.Inventory + #deltas.add > profile.Data.MaxInventorySlots then
return false, "inventory_full"
end
return true
end
applyDeltas(userId, deltas, opKey)
The applyDeltas function applies state changes to a player’s inventory based on a unique operation key. This function is used for escrow, rollback, and commit phases.
deltas
The deltas table contains add and/or remove entries.
{
add = { entries },
remove = { entries },
}
opKey
The opKey parameter ensures idempotency. Once a key has been applied, it must not be processed again.
Idempotency
Idempotency means that this function can be run repeatedly without duplicating or deleting items incorrectly. All inventory changes in applyDeltas must happen in the same profile/DataStore save as the opKey record.
appliedKeys must be saved alongside your inventory in the same record where you store it, whether that is DataStore, ProfileStore, or another profile system.
The table does not have to be named appliedKeys. You can store operation keys under any field name that fits your save format, as long as applyDeltas can check whether an opKey has already been applied before mutating inventory again.
Example
function InventoryAdapter:applyDeltas(userId, deltas, opKey)
local profile = self:getSnapshot(userId)
if not profile then
return false, "no_inventory"
end
if not profile.Data.appliedKeys then
profile.Data.appliedKeys = {}
end
if profile.Data.appliedKeys[opKey] then
return true
end
if deltas.add then
for _, entry in ipairs(deltas.add) do
table.insert(profile.Data.Inventory, entry.serverPayload)
end
end
if deltas.remove then
for _, entry in ipairs(deltas.remove) do
local uid = entry.serverPayload.uid
for i, item in ipairs(profile.Data.Inventory) do
if item.uid == uid then
table.remove(profile.Data.Inventory, i)
break
end
end
end
end
profile.Data.appliedKeys[opKey] = true
local player = Players:GetPlayerByUserId(userId)
InventoryService.save(player, profile)
return true
end
clearAppliedDeltas(userId, opKeys, context)
The optional clearAppliedDeltas function lets your adapter delete operation keys that the trade system no longer needs for retry protection. The trade system calls this only after a completed, cancelled, expired, or failed trade has finished its required commit or rollback work.
Only delete the exact keys passed in opKeys. Do not reset your entire applied-key table, because it may contain keys for other active or recoverable trades.
Example
function InventoryAdapter:clearAppliedDeltas(userId, opKeys, context)
local profile = self:getSnapshot(userId)
if not profile then
return false, "no_inventory"
end
if not profile.Data.appliedKeys then
return true
end
for _, opKey in ipairs(opKeys or {}) do
profile.Data.appliedKeys[opKey] = nil
end
local player = Players:GetPlayerByUserId(userId)
InventoryService.save(player, profile)
return true
end
The context table includes the trade id, trade state, and participant role. You usually do not need it unless your storage layer logs or audits cleanup work.
The trade system owns offer validation, escrow journal data, rollback, commit, recovery, and retry behavior. Your adapter only needs to mutate the game's inventory for add and remove, then record opKey next to the game's existing data. If clearAppliedDeltas is implemented, the trade system will prune known-safe keys for completed and recovered trades. Ancient keys that are no longer reachable from Traderie's recent trade records remain adapter-owned maintenance.