Skip to content

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)
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

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 weapons
Handlers.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 true
end
-- Legendary items can only go to auction
Handlers.CanCreateListing = function(data)
if data.rarity == 'legendary' and data.listingType ~= 'auction' then
return false, 'Legendary items must be auctioned'
end
return true
end

How to test

  1. Add a print and return false, 'Test' as in the tip above.
  2. ensure sl-marketplace.
  3. In game, open the marketplace (F6 or /market) and publish any item.
  4. You should get the message Test, the item stays in your inventory, and the console prints your line.

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 license
Handlers.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 true
end
-- 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 true
end

How 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).

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,000
Handlers.CanPlaceBid = function(data)
if data.amount > 1000000 then
return false, 'Maximum bid is $1,000,000'
end
return true
end

How 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.

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 cancelled
Handlers.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 true
end

How to test: publish something, block the guard, then cancel it from your listings in the marketplace.

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,000
Handlers.CanCreateBarterProposal = function(data)
if (data.offerAmount or 0) > 10000 then
return false, 'Barter offers cannot include more than $10,000'
end
return true
end

How 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).

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.

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 handcuffed
Handlers.CanConfirmInPersonTrade = function(data)
if Player(data.actorSource).state.handcuffed then
return false, 'You cannot trade while handcuffed'
end
return true
end

How 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.

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 reason
Handlers.CanRequestRefund = function(data)
if #(data.reason or '') < 15 then
return false, 'Please explain the problem (15 characters minimum)'
end
return true
end

How to test: buy something from another player, open your purchase history and request a refund.