Skip to content

How hooks work

SL-Marketplace ships with two open files you can edit freely, even though the rest of the script is protected by escrow:

File Runs on What it contains
server/handlers.lua Server 38 hooks: 8 guards that can block an action and 30 lifecycle hooks that tell you something happened
client/handlers.lua Client 21 events fired on the player’s own game, for cosmetic integrations

On top of those, config.lua has two callbacks: Config.Notify and Config.OnDeliveryRobbed.

Use them to connect the marketplace to the rest of your server: police dispatch, jobs, XP, achievements, logs, taxes, custom notifications, sounds…

A guard runs before an action happens and can stop it. The marketplace has already validated the request (the player has the money, the item exists, the price is in range…) but has not touched anything yet.

Handlers.CanCreateListing = function(data)
if data.price > 50000 then
return false, 'You cannot list anything above $50,000'
end
return true
end
You return Result
true or nothing The action goes ahead
false, 'reason' The action is blocked and the player sees 'reason'
false (no reason) Blocked with the default message (actionBlockedByServer in locales/, in the player’s language)
{ allowed = false, reason = '…' } Same as false, 'reason'
The function throws an error Blocked (see fail closed)

See every guard in Server guards.

A lifecycle hook runs after something has happened: a sale, a bid, an expired auction, a robbery… It cannot undo it and its return value is ignored. This is where you give XP, send logs, pay taxes or alert the police.

Handlers.OnSaleCompleted = function(data)
TriggerEvent('yourxp:addXP', data.sellerId, math.floor(data.basePrice / 100))
end

See every hook in Server hooks.

Client events — sl-marketplace:on* (client)

Section titled “Client events — sl-marketplace:on* (client)”

These fire on one player’s game (the buyer, the seller, the robber…), never broadcast to everyone. Use them for sounds, screen effects, custom notifications or UI.

AddEventHandler('sl-marketplace:onSaleMade', function(data)
PlaySoundFrontend(-1, 'WAYPOINT_SET', 'HUD_FRONTEND_DEFAULT_SOUNDSET', true)
end)

See every event in Client events.

Every server hook receives one table, data. Its fields depend on the hook and are listed for each one in this reference. Four fields are always there:

Field Type Meaning
data.hook string Name of the hook, e.g. 'OnSaleCompleted'
data.timestamp number Unix time (seconds) of the event
data.schemaVersion number Version of the data format, currently 1
data.resource string Name of the resource folder, e.g. 'sl-marketplace'

data is a copy: changing it inside your hook does not change what the marketplace does.

Most hooks give you both an identifier and a source for each player:

  • …Id (sellerId, buyerId, winnerId…) is the ESX identifier (license:…, char1:…). It is always present, even if the player is offline. Use it for anything stored.
  • …Source (sellerSource, buyerSource…) is the server ID of the player if they are online right now, or nil. Use it for things that need the player in game (notifications, events). Always check it is not nil.
Handlers.OnSaleCompleted = function(data)
if data.sellerSource then
TriggerClientEvent('chat:addMessage', data.sellerSource, { args = { 'Market', 'You sold ' .. data.itemLabel } })
end
end

Sales carry several amounts. Use the explicit ones; price and finalPrice are kept only for old integrations.

Field Meaning
basePrice Price of the listing actually paid (after a flash-sale discount), without extras
grossPaid What the buyer paid in total: basePrice + delivery, mode surcharge and insurance
platformFee Fee removed from the economy (Config.ServerFee, 5% by default)
deliveryFee modeSurcharge + npcDeliveryFee
modeSurcharge Extra for a delivery mode the seller did not prefer (Config.DeliveryBuyerFee)
npcDeliveryFee Cost of the NPC courier (Config.Gameplay.NPCDelivery.Cost)
insuranceFee Optional courier insurance
sellerPayout What the seller receives
  1. Do not rename anything. The script calls Handlers.CanCreateListing, Handlers.OnSaleCompleted… by name, and the client events by their exact string. A renamed function or event simply never fires. Do not delete them either: a missing guard counts as an error, so the action it protects is always blocked. /markethandlers validate tells you if one is missing.
  2. Keep hooks fast. They run inside the marketplace operation. Do not use Wait() or long database queries in guards. If a hook takes longer than Config.HandlerRuntime.SlowWarningMs (100 ms), the console warns you. For slow work in lifecycle hooks, start a thread: CreateThread(function() ... end).
  3. Errors are contained. If your code throws an error, the console prints it with a traceback and the marketplace operation carries on (for guards: it is blocked, see below). A bug in your hook never breaks a sale halfway.
  4. Lifecycle hooks cannot cancel anything. If you need to stop an action, use the matching guard.

If a guard throws an error, the action is blocked. This is on purpose: if your jobs or permissions integration breaks, it must not turn into a way to skip your rules.

You can change it in config.lua, but only for guards that are not about security:

Config.HandlerRuntime = {
GuardFailurePolicy = 'deny', -- 'allow' lets the action through when a guard crashes
}
Option Default What it does
Debug false Prints every hook execution and how long it took
SlowWarningMs 100 Warns in the console when a hook takes longer than this (ms). 0 disables it
TrackMetrics true Counts calls, errors, denials and timings for /markethandlers
ValidateOnStart true Checks on start that every hook exists and is a function
EnableDiagnosticsCommand true Registers the diagnostics command
DiagnosticsCommand 'markethandlers' Name of the diagnostics command
GuardFailurePolicy 'deny' What happens when a guard throws an error: 'deny' or 'allow'
DefaultDenyMessage (not set) Fixed message when a guard blocks without a reason. Not set = the translated actionBlockedByServer