qb-pawnshop
qb-pawnshop buys stolen goods for cash and melts jewellery into raw materials over a timer. It is
the usual sink for goldchain, diamond_ring, and rolex, and the usual source of goldbar and
diamond on a stock server.
It is the cash-out counterpart to the robbery resources. Compare with qb-shops, which sells legal goods to players rather than buying illegal ones from them.
Before deploying it, read Client-supplied arguments. The selling event trusts price and reward values sent by the client. The server-authority checklist covers what to check instead.
The manifest at the commit linked above declares version 1.5.0.
Dependencies and start order
From the manifest: @qb-core/shared/locale.lua, config.lua, and locales as shared scripts,
@oxmysql/lib/MySQL.lua and server.lua on the server, and PolyZone client.lua, BoxZone.lua,
ComboZone.lua plus client.lua on the client.
From the implementation: qb-inventory for item mutation, qb-log
for the ban log line, and the bans table for the ban itself.
Start order: oxmysql, qb-core, qb-inventory, PolyZone, then qb-pawnshop.
Configuration
| Key | Default | Meaning |
|---|---|---|
Config.PawnLocation | table | Shop coordinates used for the proximity check |
Config.BankMoney | false | true pays into the bank instead of cash |
Config.UseTimes | false | true restricts the shop to opening hours |
Config.TimeOpen | 7 | Opening hour, only used when UseTimes is true |
Config.TimeClosed | 17 | Closing hour |
Config.SendMeltingEmail | true | Email the player when melting finishes |
Config.UseTarget | from the UseTarget convar | Set setr UseTarget true in server.cfg |
Config.PawnItems | eight entries | Item to buy price |
Config.MeltingItems | table | Item to reward list and meltTime in minutes |
Each Config.PawnItems price is written as math.random(50, 100). That expression is evaluated once
when the file loads, so every item gets a fixed price for the lifetime of the resource, and every
item is priced in the same 50 to 100 band. Restarting the resource reshuffles them. Replace the calls
with literals if you want stable, per-item pricing.
meltTime is in minutes and the shipped jewellery entries use 0.15, which is nine seconds. Raise
it before going live if melting is meant to be a real time cost.
Server surface
| Handler | Kind | Arguments |
|---|---|---|
qb-pawnshop:server:sellPawnItems | net event | itemName, itemAmount, itemPrice |
qb-pawnshop:server:meltItemRemove | net event | itemName, itemAmount, item |
qb-pawnshop:server:pickupMelted | net event | item |
qb-pawnshop:server:getInv | callback | none, returns the player’s items |
Client-supplied arguments
This is the part to address before running the resource on a public server. Three of the four handlers take values from the client and use them without comparing them to the server’s own config.
Sell price
sellPawnItems computes tonumber(itemAmount) * itemPrice and pays that amount. itemPrice is
never checked against Config.PawnItems. Look the price up on the server instead:
-- patched into server.lua
local function GetPawnPrice(itemName)
for _, entry in pairs(Config.PawnItems) do
if entry.item == itemName then return entry.price end
end
end
local price = GetPawnPrice(itemName)
if not price then return end
local amount = math.floor(tonumber(itemAmount) or 0)
if amount <= 0 then return end
-- remove the items first, then pay amount * priceMelting rewards
pickupMelted receives an item table and iterates its nested reward entries, granting
meltedAmount * rewardAmount of whatever item name the table names. Nothing in that table is
validated against Config.MeltingItems, so the reward item and quantity are both decided by the
caller.
The fix is the same shape: have the server record what a given player put into the melter, keyed by
source, and read the reward list from Config.MeltingItems on pickup. The one-shot token pattern in
qb-vineyard is a direct model for this.
Melt input
meltItemRemove takes an item table and uses item.time to compute the melt duration sent back
to the client. A forged value shortens the wait.
The proximity check
All three money and item paths run the same loop:
local dist
for _, value in pairs(Config.PawnLocation) do
dist = #(playerCoords - value.coords)
if #(playerCoords - value.coords) < 2 then
dist = #(playerCoords - value.coords)
break
end
end
if dist > 5 then
exploitBan(src, '...')
return
endPosition is read from the server’s own view of the ped, which is right. The loop itself is not: when
no location is within 2 units it does not break, so dist ends up holding the distance to whichever
location pairs happened to visit last. With more than one shop configured, a player standing at
shop A can be measured against shop B and banned. With exactly one shop it behaves as intended.
Bans are written to the bans table with bannedby = 'qb-pawnshop' and expire = 2147483647. That
table is not created by this resource. Confirm it exists before enabling.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Players banned at a legitimate shop | More than one entry in Config.PawnLocation, see the loop above |
| Prices change after every restart | Config.PawnItems prices are math.random calls evaluated at load |
| All items sell for a similar amount | Same cause. Replace the calls with literals |
| Melting finishes almost instantly | meltTime is in minutes and ships as 0.15 |
| Implausible payouts or reward items | Price and reward tables are accepted from the client. Apply the fixes above |
| Ban insert fails | The bans table is missing or has different columns |
| Shop always closed | Config.UseTimes is true and the in-game hour is outside TimeOpen to TimeClosed |
Proposed manual smoke test
Not yet executed. On staging, with a database backup:
- Confirm the
banstable exists, and that every item inConfig.PawnItemsandConfig.MeltingItemsexists in your shared items. - Sell one item and record the price, restart the resource, and confirm the price changed.
- Melt one item and confirm the reward matches
Config.MeltingItems. - Configure two shop locations and confirm whether legitimate selling triggers a ban.
- After applying the server-side price and reward lookups, confirm payouts match the config.
- Clear any test bans afterwards.