Server guards
Guards live in server/handlers.lua and are named Handlers.Can…. Each one runs after the marketplace has checked the request and before anything changes (money, items, database). Return false, 'reason' to block the action; the player sees the reason. Read How hooks work for the return rules.
Every guard receives these fields, plus the ones listed for it:
| Field | Type | Meaning |
|---|---|---|
actorSource |
number | Server ID of the player doing the action |
actorId |
string | Their identifier |
actorName |
string | Their player name |
action |
string | What they are trying to do (listed for each guard) |
Summary
Section titled “Summary”| Guard | Protects | action values |
|---|---|---|
CanCreateListing |
Publishing a listing of any type | create_listing |
CanBuyListing |
Buying, Buy Now, and accepting a reverse-auction offer | buy_listing, accept_reverse_offer |
CanPlaceBid |
Manual bids and proxy (automatic) bids | place_bid, set_proxy_bid |
CanCancelListing |
The seller cancelling their own listing | cancel_listing |
CanCreateBarterProposal |
Barter proposals and seller counteroffers | create_barter_proposal, create_barter_counter |
CanAcceptBarterProposal |
Accepting a barter proposal or counteroffer | accept_barter_proposal |
CanConfirmInPersonTrade |
Each player’s confirmation in an in-person trade | confirm_in_person_trade |
CanRequestRefund |
A buyer opening a refund request | request_refund |
CanCreateListing
Section titled “CanCreateListing”When: a player publishes a listing (fixed price, auction, blind, Dutch, reverse, bundle or barter), after the item, quantity, price, duration and delivery modes have been validated and before the item is taken from their inventory.
Blocking it: nothing is taken from the player; the listing is not created.
| Field | Meaning |
|---|---|
sellerSource, sellerId, sellerName |
The seller (sellerName is their shop name) |
item, itemLabel |
Item name and visible label |
itemKind |
Kind of item (normal item, weapon, bundle…) |
itemMetadata |
Item metadata (serial, durability…), or nil |
category, rarity |
Detected category and rarity |
quantity |
Units |
price / basePrice |
Price, or starting price for auctions |
buyNowPrice |
Buy Now price, or nil |
listingType |
'fixed', 'auction', 'blind', 'dutch', 'reverse', 'bundle' or 'barter' |
duration |
Duration in minutes |
deliveryMode |
Seller’s preferred delivery mode |
deliveryModes |
Every delivery mode the seller allows |
meetupPoint |
Meetup point ID, if any |
featured, featuredCost |
Whether the seller pays to feature it, and how much |
images |
Photo URLs |
bundleItems |
Items of a bundle |
wantedItems, wantsAny |
What a barter listing asks for |
Examples
-- Only police can sell weaponsHandlers.CanCreateListing = function(data) if data.category == 'weapons' then local xPlayer = ESX.GetPlayerFromId(data.actorSource) if not xPlayer or xPlayer.job.name ~= 'police' then return false, 'Only the police can sell weapons here' end end return trueend-- Legendary items can only go to auctionHandlers.CanCreateListing = function(data) if data.rarity == 'legendary' and data.listingType ~= 'auction' then return false, 'Legendary items must be auctioned' end return trueendHow to test
- Add a
printandreturn false, 'Test'as in the tip above. ensure sl-marketplace.- In game, open the marketplace (
F6or/market) and publish any item. - You should get the message Test, the item stays in your inventory, and the console prints your line.
CanBuyListing
Section titled “CanBuyListing”When: a player buys a fixed-price listing, a bundle, a Dutch auction or uses Buy Now on an auction, after their money and the delivery mode are checked and before any money moves. It also runs when the author of a reverse auction accepts a seller’s offer.
Blocking it: no money is taken and the listing stays available.
| Field | Meaning |
|---|---|
action |
'buy_listing', or 'accept_reverse_offer' for reverse auctions |
purchaseType |
'direct', 'buy_now' or 'reverse_offer' |
buyerSource, buyerId, buyerName |
The buyer |
sellerSource, sellerId, sellerName |
The seller (sellerSource is nil if offline) |
item, itemLabel, itemMetadata, category, rarity, quantity |
What is being bought |
basePrice |
Listing price being paid (with flash-sale discount applied) |
grossPaid |
Total the buyer will pay, extras included |
deliveryFee, modeSurcharge, npcDeliveryFee, insuranceFee |
Extras (see money fields) |
deliveryMode |
Mode chosen by the buyer: 'mailbox', 'meetup', 'npc' or 'person' |
listingType |
Type of listing |
messageId |
Only for accept_reverse_offer: the offer message |
listingId |
Listing ID |
Examples
-- Players cannot buy from themselves through an alt account on the same licenseHandlers.CanBuyListing = function(data) local buyer = GetPlayerIdentifierByType(data.buyerSource, 'license') if data.sellerSource and buyer == GetPlayerIdentifierByType(data.sellerSource, 'license') then return false, 'You cannot buy from your own account' end return trueend-- Courier delivery only for players level 5+Handlers.CanBuyListing = function(data) if data.deliveryMode == 'npc' and exports['your_levels']:GetLevel(data.buyerSource) < 5 then return false, 'Courier delivery unlocks at level 5' end return trueendHow to test: block it with the test snippet, then buy any listing. To test alone, turn on Config.Debug = true and create a listing from a test seller with /markettest listing mailbox fixed bread 50, then buy it (see Testing hooks).
CanPlaceBid
Section titled “CanPlaceBid”When: a player places a bid on an auction (action = 'place_bid') or sets a maximum for proxy bidding (action = 'set_proxy_bid'), after the amount and their money are checked.
Blocking it: the bid is not placed and no money is reserved.
| Field | Meaning |
|---|---|
action |
'place_bid' or 'set_proxy_bid' |
bidType |
'manual' or 'proxy' |
bidderSource, bidderId, bidderName |
The bidder |
sellerSource, sellerId, sellerName |
The seller |
item, itemLabel, quantity |
The item |
amount |
The bid, or the proxy maximum |
currentBid |
Current highest bid |
minimumBid |
Minimum valid bid right now |
listingType |
'auction' or 'blind' |
listingId |
Listing ID |
Example
-- Cap bids at $1,000,000Handlers.CanPlaceBid = function(data) if data.amount > 1000000 then return false, 'Maximum bid is $1,000,000' end return trueendHow to test: block it with the test snippet, open an auction and bid. Try both a normal bid and a proxy maximum to see both action values. Solo: /markettest listing mailbox auction bread 100 creates a 5-minute auction you can bid on.
CanCancelListing
Section titled “CanCancelListing”When: the seller cancels their own listing, before it is marked cancelled and the item returned. Admin cancellations from the panel do not go through this guard.
Blocking it: the listing stays active.
| Field | Meaning |
|---|---|
sellerSource, sellerId, sellerName |
The seller |
item, itemLabel, itemMetadata, quantity |
The item |
price |
Listing price |
listingType |
Type of listing |
listingId |
Listing ID |
Example
-- Auctions with bids cannot be cancelledHandlers.CanCancelListing = function(data) if data.listingType == 'auction' then local bids = MySQL.scalar.await('SELECT COUNT(*) FROM marketplace_bids WHERE listing_id = ?', { data.listingId }) if bids and bids > 0 then return false, 'You cannot cancel an auction that already has bids' end end return trueendHow to test: publish something, block the guard, then cancel it from your listings in the marketplace.
CanCreateBarterProposal
Section titled “CanCreateBarterProposal”When: a buyer sends a barter proposal on a barter listing (action = 'create_barter_proposal'), or the seller answers with a counteroffer (action = 'create_barter_counter'). It runs before the offered items are held in escrow.
Blocking it: no items or money are held and no proposal is sent.
| Field | Meaning |
|---|---|
action |
'create_barter_proposal' or 'create_barter_counter' |
proposalType |
'proposal' or 'counter' |
recipientSource, recipientId |
Who receives the proposal |
sellerSource, sellerId, sellerName |
The owner of the barter listing |
item, itemLabel, itemMetadata, quantity |
The listed item |
offeredItems |
Items offered (empty in a counteroffer) |
wantedItems |
Items asked for (only in a counteroffer) |
offerAmount |
Money added to the offer |
deliveryMode |
Chosen delivery mode (nil in a counteroffer) |
messageId |
Proposal being countered, or nil |
listingId |
Listing ID |
Example
-- No money in barter proposals above $10,000Handlers.CanCreateBarterProposal = function(data) if (data.offerAmount or 0) > 10000 then return false, 'Barter offers cannot include more than $10,000' end return trueendHow to test: needs two players. Player A publishes a barter listing; player B sends a proposal (test create_barter_proposal); player A answers with a counteroffer (test create_barter_counter).
CanAcceptBarterProposal
Section titled “CanAcceptBarterProposal”When: the seller accepts a proposal, or the buyer accepts the seller’s counteroffer, before the listing and both sides’ items are locked.
Blocking it: the proposal stays open.
| Field | Meaning |
|---|---|
acceptanceType |
'proposal' (seller accepts) or 'counter' (buyer accepts a counteroffer) |
buyerSource, buyerId |
The player who made the proposal |
sellerSource, sellerId, sellerName |
The listing owner |
proposalFromId, proposalToId |
Author and recipient of the message being accepted |
item, itemLabel, itemMetadata, quantity |
The listed item |
offeredItems, wantedItems, offerAmount |
Contents of the deal |
messageId, listingId |
IDs |
How to test: two players. B sends a proposal to A’s barter listing, block the guard, A accepts it and sees the message. Repeat with a counteroffer accepted by B.
CanConfirmInPersonTrade
Section titled “CanConfirmInPersonTrade”When: during an in-person trade (or a barter settled in person), each player presses the button to confirm the exchange. It runs once per player, before that confirmation counts.
Blocking it: that player’s confirmation is not recorded; the trade stays open.
| Field | Meaning |
|---|---|
role |
'buyer' or 'seller': who is confirming |
partnerSource, partnerId |
The other player |
buyerSource, buyerId, buyerName |
The buyer |
sellerSource, sellerId, sellerName |
The seller |
item, itemLabel, quantity, price |
The deal |
tradeKind |
'person' or 'barter' |
tradeId, listingId |
IDs |
Example
-- No trades while either player is handcuffedHandlers.CanConfirmInPersonTrade = function(data) if Player(data.actorSource).state.handcuffed then return false, 'You cannot trade while handcuffed' end return trueendHow to test: two players. B buys A’s listing with the in person delivery mode, they agree a meeting point in the chat, meet there and press confirm.
CanRequestRefund
Section titled “CanRequestRefund”When: a buyer opens a refund request on a past purchase (Config.Refunds.Enabled must be true and the purchase inside Config.Refunds.WindowDays).
Blocking it: the request is not created.
| Field | Meaning |
|---|---|
buyerSource, buyerId, buyerName |
The buyer |
sellerSource, sellerId, sellerName |
The seller |
item, itemLabel, quantity |
What was bought |
originalPrice |
Price paid |
reason |
Text written by the buyer |
historyId, listingId |
Purchase history entry and listing |
Example
-- Refunds need a real reasonHandlers.CanRequestRefund = function(data) if #(data.reason or '') < 15 then return false, 'Please explain the problem (15 characters minimum)' end return trueendHow to test: buy something from another player, open your purchase history and request a refund.