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…
The three kinds of hooks
Section titled “The three kinds of hooks”Guards — Handlers.Can* (server)
Section titled “Guards — Handlers.Can* (server)”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 trueend| 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.
Lifecycle hooks — Handlers.On* (server)
Section titled “Lifecycle hooks — Handlers.On* (server)”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))endSee 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.
The data table
Section titled “The data table”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.
Identifiers and sources
Section titled “Identifiers and sources”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, ornil. Use it for things that need the player in game (notifications, events). Always check it is notnil.
Handlers.OnSaleCompleted = function(data) if data.sellerSource then TriggerClientEvent('chat:addMessage', data.sellerSource, { args = { 'Market', 'You sold ' .. data.itemLabel } }) endendMoney fields
Section titled “Money fields”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 |
- 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 validatetells you if one is missing. - Keep hooks fast. They run inside the marketplace operation. Do not use
Wait()or long database queries in guards. If a hook takes longer thanConfig.HandlerRuntime.SlowWarningMs(100 ms), the console warns you. For slow work in lifecycle hooks, start a thread:CreateThread(function() ... end). - 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.
- Lifecycle hooks cannot cancel anything. If you need to stop an action, use the matching guard.
Guards fail closed
Section titled “Guards fail closed”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}Config.HandlerRuntime
Section titled “Config.HandlerRuntime”| 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 |
Next steps
Section titled “Next steps”- Server guards: the 8 guards, field by field.
- Server hooks: the 30 lifecycle hooks and the two
config.luacallbacks. - Client events: the 21 client events.
- Testing hooks: how to see every hook fire, step by step.