Skip to content

Testing hooks

This page shows how to check that your hooks work before going live: how to see each one fire, the built-in test commands, how to shorten the timers and a checklist of every hook.

  1. Enable the test commands in config.lua:

    Config.Debug = true

    This turns on /markettest and /marketdebug. Both only work for admins.

  2. Make yourself admin in server.cfg (or add your identifier to Config.Admin.Identifiers):

    server.cfg
    add_ace group.admin marketplace.admin allow
    add_principal identifier.license:YOUR_LICENSE group.admin
  3. Optional: trace every hook with its duration in the console:

    Config.HandlerRuntime = {
    Debug = true,
    -- keep the rest as it is
    }
  4. Restart the resource after every change to config.lua or handlers.lua:

    Terminal window
    ensure sl-marketplace

    The console should print handlers API ready. If it says the handlers API is incomplete, a hook was renamed or deleted (see /markethandlers validate).

Many hooks need two players (a buyer and a seller, a robber and a buyer). You can open a second FiveM client on the same PC with the -cl2 launch option:

  1. Create a shortcut to FiveM.exe and add -cl2 at the end of Target.
  2. Open FiveM normally, then open the shortcut: it starts a second client with its own identity.
  3. Connect both to your test server (localhost:30120).

Paste this block at the very end of server/handlers.lua. It wraps every hook so it prints its name and full data in the server console, then runs your code as usual (guards keep blocking exactly as before).

server/handlers.lua
-- ─── TEST ONLY: print every hook. Delete this block before going live. ───
local function printable(value, depth)
depth = depth or 0
local kind = type(value)
if kind == 'vector2' or kind == 'vector3' or kind == 'vector4' then return tostring(value) end
if kind ~= 'table' then return value end
if depth > 5 then return '...' end
local out = {}
for k, v in pairs(value) do out[k] = printable(v, depth + 1) end
return out
end
for name, original in pairs(Handlers) do
Handlers[name] = function(data)
print(('^3[hook] %s^7\n%s'):format(name, json.encode(printable(data), { indent = true })))
return original(data)
end
end

Restart the resource and do anything in the marketplace: every hook shows up with its data, for example:

[hook] OnSaleCompleted
{
"buyerName": "Ruben",
"item": "bread",
"basePrice": 50,
"grossPaid": 50,
"deliveryMode": "mailbox",
"stage": "settled",
...
}

Paste this at the end of client/handlers.lua. The prints appear in the F8 console of the player who receives each event.

client/handlers.lua
-- ─── TEST ONLY: print every client event. Delete before going live. ───
local EVENTS = {
'onPurchaseCompleted', 'onDeliveryArrived', 'onDeliveryResult', 'onAuctionWon', 'onSaleMade',
'onInPersonTradeCompleted', 'onListingCreated', 'onListingCancelled', 'onListingBumped',
'onBidPlaced', 'onOutbid', 'onMailboxClaimed', 'onMessageReceived', 'onRefundStatusChanged',
'onInPersonTradeStarted', 'onInPersonMeetUpdated', 'onInPersonTradeCancelled', 'onDeliveryRobbed',
'onSkillCheckStarted', 'onSkillCheckPhase', 'onSkillCheckFinished',
}
local function printable(value, depth)
depth = depth or 0
local kind = type(value)
if kind == 'vector2' or kind == 'vector3' or kind == 'vector4' then return tostring(value) end
if kind ~= 'table' then return value end
if depth > 5 then return '...' end
local out = {}
for k, v in pairs(value) do out[k] = printable(v, depth + 1) end
return out
end
for _, name in ipairs(EVENTS) do
AddEventHandler('sl-marketplace:' .. name, function(data)
print(('[client event] %s %s'):format(name, json.encode(printable(data))))
end)
end

Built-in diagnostics for the hooks. Works from the server console or in game for admins. Enabled with Config.HandlerRuntime.EnableDiagnosticsCommand (on by default); the name is Config.HandlerRuntime.DiagnosticsCommand.

Command What it shows
/markethandlers Summary: total calls, successes, errors, guard denials and slow calls
/markethandlers all One line per hook: calls, errors, denials, slow calls, average and maximum time
/markethandlers errors Only hooks with errors, slow calls or missing
/markethandlers validate Checks that all 38 hooks exist and are functions. Lists missing, invalid and extra ones
/markethandlers reset Resets the counters

Example output of all:

[sl-marketplace][handler diagnostics] OnSaleCompleted lifecycle calls=3 ok=3 errors=0 denials=0 slow=0 avg=0.33ms max=1ms
[sl-marketplace][handler diagnostics] CanBuyListing guard calls=4 ok=4 errors=0 denials=1 slow=0 avg=0.25ms max=1ms

A good routine: reset, do the action you are testing, all. The hook you expect should have calls=1; if errors goes up, the console above has the error and its traceback.

Test commands to try hooks alone, without a second player. Needs Config.Debug = true and admin; run them in game (not from the console).

Command What it does
/markettest help Shows the usage
/markettest listing [mode] [type] [item] [price] [qty] Creates a listing sold by a test seller (a fake player) so you can buy it or bid on it yourself
/markettest grant [money] [item] [qty] Gives you money and/or items
/markettest clean Deletes the test seller and all their listings

For listing:

Parameter Values Default
mode mailbox, meetup, npc mailbox
type fixed, auction, blind, dutch, bundle fixed
item Any item name bread
price Price (never below Config.MinPrice) 10
qty Quantity 1

Test auctions (auction, blind) end after 5 minutes. Examples:

/markettest grant 5000
/markettest listing mailbox fixed bread 50 -> buy it: CanBuyListing, OnSaleCompleted
/markettest listing npc fixed water 100 -> buy it: courier van, OnDeliveryCompleted
/markettest listing mailbox auction bread 100 -> bid, wait 5 min: OnAuctionEnded
/markettest listing mailbox dutch bread 1000 -> buy it: OnSaleCompleted with listingType dutch
/markettest clean

Some hooks only fire after a timer. On the test server, lower these values temporarily:

To test Change Test value
Expiry of your own auctions Config.Auction.MinDurationMinutes 1
Dutch price drops Config.Auctions.Dutch.MinIntervalMinutes 1
Reverse and barter expiry Create them with the shortest duration —
Courier timeout Config.Gameplay.NPCDelivery.DeliveryTimeout 30
In-person trade timeout Config.InPersonTrade.TimeoutMinutes 2
Meeting proposal expiry Config.InPersonTrade.MeetRequestTimeoutSeconds 30
Abandoning the meeting area Config.InPersonTrade.AbandonTimeoutSeconds 15
Bumping several times Config.RateLimits.bump { max = 10, window = 60 }

Two timers cannot be shortened from config.lua: expired listings and finished auctions are checked every 30 seconds, and automatic refund approval (AutoApproveAfterHours) is checked every hour.

Make it block everything, check the message, then put your logic back:

Handlers.CanPlaceBid = function(data)
print(('[test] %s by %s: $%s (minimum $%s)'):format(data.action, data.bidderName, data.amount, data.minimumBid))
return false, 'Test: bids are blocked'
end

What you should see: the message Test: bids are blocked in game, your line in the console, nothing changes (no money reserved), and /markethandlers all shows denials=1 for CanPlaceBid.

Also test the failure case: make the guard throw an error (error('boom')). The action must be blocked and the console must print the error with a traceback. That confirms a broken integration will never let actions through (unless you set GuardFailurePolicy = 'allow').

Every hook, how to trigger it and whether you can do it alone.

Hook How to trigger it Alone?
CanCreateListing Publish a listing from the marketplace Yes
CanBuyListing Buy a listing (/markettest listing) Yes
CanPlaceBid Bid, or set a proxy maximum, on a test auction Yes
CanCancelListing Cancel one of your listings Yes
CanCreateBarterProposal Send a proposal on a barter listing / counter it 2 players
CanAcceptBarterProposal Accept a barter proposal or counteroffer 2 players
CanConfirmInPersonTrade Confirm an in-person trade 2 players
CanRequestRefund Request a refund on a purchase 2 players
Hook How to trigger it Alone?
OnSaleCompleted Buy a test listing Yes
OnAuctionEnded Bid on a test auction and wait 5 min Yes
OnReverseOfferAccepted Accept an offer on your reverse auction 2 players
OnListingCreated Publish from the marketplace Yes
OnListingExpired Test auction with no bids, wait 5 min Yes
OnListingCancelled Cancel a listing (and from /marketadmin) Yes
OnListingBumped Bump a listing Yes
OnBidPlaced Bid on a test auction Yes
OnProxyBidSet Set a proxy maximum Yes
OnDeliveryCompleted Buy an npc test listing, wait for the van Yes
OnDeliveryFailed Destroy the van, or disconnect during the delivery Yes
OnDeliveryRobbed Rob another player’s courier van 2 players
OnInPersonTradeCompleted Complete an in-person trade 2 players
OnInPersonTradeCancelled Cancel an in-person trade 2 players
OnMeetupStatusChanged Propose / accept a meeting point in the chat 2 players
OnBarterProposalCreated Send a barter proposal 2 players
OnBarterProposalStatusChanged Accept, counter or reject a proposal 2 players
OnRefundRequested Request a refund 2 players
OnRefundApproved Approve a refund (or force it in /marketadmin) 2 players
OnRefundRejected Reject a refund 2 players
OnPlayerRated Rate a seller after buying 2 players
OnMessageSent Send a message on a listing 2 players
OnFavoriteChanged Favourite a listing Yes
OnFollowChanged Follow a seller 2 players
OnWishlistChanged Add an item to your wishlist Yes
OnReportCreated Report a listing Yes
OnProfileCreated Create your shop for the first time Yes
OnProfileUpdated Edit your profile Yes
OnMailboxClaimed Collect an item or money from the mailbox Yes
OnAdminAction Cancel, ban, unban, resolve a report or force a refund in /marketadmin Yes
Event Receives it Alone?
onPurchaseCompleted Buyer Yes
onSaleMade Seller 2 players
onAuctionWon Winner Yes
onListingCreated, onListingCancelled, onListingBumped Seller Yes
onBidPlaced Bidder Yes
onOutbid Previous leader 2 players
onMailboxClaimed Player collecting Yes
onMessageReceived Recipient 2 players
onRefundStatusChanged Buyer and seller 2 players
onDeliveryArrived, onDeliveryResult Buyer Yes
onDeliveryRobbed Robber 2 players
onInPersonTradeStarted, onInPersonMeetUpdated, onInPersonTradeCompleted, onInPersonTradeCancelled Both 2 players
onSkillCheckStarted, onSkillCheckPhase, onSkillCheckFinished Robber 2 players

My hook never fires. Check the name is exactly the same as in the original file (capital letters included) and that you restarted the resource. /markethandlers validate lists hooks that are missing or were renamed. For client events, check the event string starts with sl-marketplace:.

Every action is blocked after editing a guard. Your guard is throwing an error, and guards fail closed. The server console shows [sl-marketplace][handlers][ERROR] CanXxx: with the traceback.

The console says Slow execution: 250 ms. A hook took longer than SlowWarningMs. Move slow work (HTTP requests, big queries) into CreateThread(function() ... end) inside the hook, and never do it in a guard.

A client event does not arrive. Most of them only go to one player (see Client events). Seller-side events do not fire for the /markettest seller, because it is never online.

/markettest says Config.Debug is disabled. Set Config.Debug = true and restart the resource. Remember to set it back to false in production.