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.
Prepare the server
Section titled “Prepare the server”-
Enable the test commands in
config.lua:Config.Debug = trueThis turns on
/markettestand/marketdebug. Both only work for admins. -
Make yourself admin in
server.cfg(or add your identifier toConfig.Admin.Identifiers):server.cfg add_ace group.admin marketplace.admin allowadd_principal identifier.license:YOUR_LICENSE group.admin -
Optional: trace every hook with its duration in the console:
Config.HandlerRuntime = {Debug = true,-- keep the rest as it is} -
Restart the resource after every change to
config.luaorhandlers.lua:Terminal window ensure sl-marketplaceThe console should print
handlers API ready. If it saysthe handlers API is incomplete, a hook was renamed or deleted (see/markethandlers validate).
Two players on one PC
Section titled “Two players on one PC”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:
- Create a shortcut to
FiveM.exeand add-cl2at the end of Target. - Open FiveM normally, then open the shortcut: it starts a second client with its own identity.
- Connect both to your test server (
localhost:30120).
1. Log every hook
Section titled “1. Log every hook”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).
-- ─── 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 outend
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) endendRestart 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", ...}2. Log every client event
Section titled “2. Log every client event”Paste this at the end of client/handlers.lua. The prints appear in the F8 console of the player who receives each event.
-- ─── 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 outend
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/markethandlers
Section titled “/markethandlers”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=1msA 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.
/markettest
Section titled “/markettest”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 cleanShorten the timers
Section titled “Shorten the timers”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.
Test a guard
Section titled “Test a guard”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'endWhat 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').
Checklist
Section titled “Checklist”Every hook, how to trigger it and whether you can do it alone.
Server guards
Section titled “Server guards”| 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 |
Server hooks
Section titled “Server hooks”| 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 |
Client events
Section titled “Client events”| 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 |
Troubleshooting
Section titled “Troubleshooting”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.