Vehicle Market
Version 1.0.0. The listing, or pick it up in the shop and run it.
Vehicle Market
A used car market for ESX, Qbox and QBCore. Owned cars lose value with age, mileage and damage, players sell them to each other at prices the market keeps honest, and a dealer buys any car back at a floor price, so a car is never worthless.
What you get
- Real depreciation. Value starts at the new price in your vehicle shop, then drops with the days since the market first saw the car, the kilometres it has driven and its engine and body damage. Each class can lose value faster or slower. Also, a floor share means a car never drops below a set part of its new price.
- Mileage measured on the server. Neither ESX nor QBCore keeps an odometer that anything writes, so the market measures it itself: it reads where each driver's car is every few seconds and adds up the distance. Teleports, respawns and garage spawns don't count, and passengers don't add distance.
- Player listings. Players list a garaged car at a price between a minimum and a maximum share of its value and pay a small listing fee. The listing comes down by itself after a set time.
- Safe handover. The server reads every listing from the database, never from the client. A purchase claims the listing for one buyer, checks that the seller still owns the car and that it's in a garage (not out on the street), takes the money, changes the owner in one guarded update, and only then pays the seller. If anything fails along the way, the buyer gets the money back.
- Dealer buyback. Sell any garaged car to the dealer for a share of its value, with a daily limit.
- A valuation tool. "What is my car worth" shows the value, the new price, how much age, mileage and condition took off, what the dealer pays and the price range allowed for a listing.
- Admin view. Sales volume, the average price against value, dealer buybacks per day, and flagged sales (prices far from value, and the same two characters trading over and over).
- Nothing to import by hand, the tables create themselves on start.
Requirements
- ESX Legacy, QBCore or Qbox
- oxmysql, MySQL 5.7+ or MariaDB 10.2+
- A garage that marks parked cars:
owned_vehicles.storedon ESX (esx_garage and most others),player_vehicles.stateon QBCore and Qbox (qb-garages, qbx_garages and most others) - OneSync (the default on current servers), for the mileage and the "car is out on the street" check
Install
- Unzip so that
resources/vehicle_market/fxmanifest.luaexists (notresources/vehicle_market-v1.0.0/vehicle_market/..., which Windows "Extract All" creates), then addensure vehicle_marketafterensure oxmysql. Start order against your framework doesn't matter, it waits up to 30 seconds and prints what it found, for examplemarket open on qbcore, 214 models priced. - Give staff the admin view in
server.cfg:add_ace group.admin vehiclemarket.admin allow - Start the server. The four
vehicle_market_*tables create themselves. - Players open the market with
/market. If you'd rather open it from a dealer ped or a target zone, callexports.vehicle_market:OpenMarket(source)from a server script.
Configure
Everything is in config.lua, with a comment on each setting. These are the ones most servers change:
| Setting | What it does |
|---|---|
Config.Account |
Money account used for buying, selling, fees and buybacks (bank by default) |
Config.Prices |
New price per model, for models your shop doesn't sell or to override one |
Config.Depreciation.age / .mileage |
How much of the price is kept after so many days or kilometres |
Config.Depreciation.classes |
How fast each class loses value (1.0 normal, above 1 faster) |
Config.Depreciation.floor |
Lowest share of the new price a car can be worth |
Config.Listing.minPercent / maxPercent |
Allowed asking price as a share of the value |
Config.Listing.feePercent, feeMin, feeMax, expiryHours |
Listing fee and how long a listing stays up |
Config.Buyback.percent, perDay, holdHours |
What the dealer pays and how often |
Config.Flags |
When the admin view flags a sale |
Keep Config.Buyback.percent below Config.Listing.minPercent. Otherwise a player can buy a car at the lowest
allowed price and sell it straight to the dealer for more. The server prints a warning on start when the config
allows that, and also when a depreciation curve doesn't go up in days or kilometres.
Where new prices come from
| Framework | Source |
|---|---|
| ESX | the vehicles table (name, model, price, category) from the ESX Legacy SQL and esx_vehicleshop. Set Config.EsxShopTable if your shop uses another table with those columns |
| QBCore | QBCore.Shared.Vehicles (qb-core shared/vehicles.lua) |
| Qbox | qbx_core's vehicle list (exports.qbx_core:GetVehiclesByName()) |
A model that's in none of these and not in Config.Prices has no value, so it can't be listed or sold to the
dealer (set Config.DefaultPrice to give those cars a value anyway).
How age and mileage start
Age counts from the first time the market sees a car (when its owner opens the market, or when someone drives it), so on a server that just installed the market every car starts at day 0 and 0 km. That's on purpose, since the market can't tell how old a car is from the framework's tables. Selling a car on the market doesn't reset its age or mileage.
Frameworks, garages and what is checked
| ESX | QBCore | Qbox | |
|---|---|---|---|
| Owned cars | owned_vehicles |
player_vehicles |
player_vehicles (qbx_vehicles) |
| Owner column changed on a sale | owner |
citizenid and license |
citizenid and license |
| "In a garage" | stored = 1 |
state = 1 |
state = 1 |
| Condition | engineHealth and bodyHealth in the vehicle JSON |
engine and body columns |
engine and body columns |
| Never sold | job cars (job set) |
cars with finance owed (balance above 0, qb-vehicleshop) |
Garages can't be locked from outside, and some let the owner's client mark a car as parked while it's still out (qb-garages does). So the market checks at the moment of sale: the car has to still belong to the seller, has to be marked as garaged, and no vehicle with that plate may exist anywhere in the world. If the seller takes a listed car out, it disappears from the market until it's parked again, then comes back by itself. If the car leaves the seller some other way (sold in another script, given away, deleted), the listing closes.
The bought car stays in the garage it was parked in. On Qbox the owner change is done with one guarded SQL update
instead of qbx_vehicles:SetPlayerVehicleOwner, because that export doesn't check who owns the car or where it is.
So a changeVehicleOwner hook you registered in qbx_vehicles isn't called for market sales.
Money and restarts
- The buyer pays only after the listing is claimed for them and the car is confirmed. If the owner change then fails (IE the seller moved the car in the same instant), the buyer is refunded and the listing closes.
- The seller is paid only after the owner change succeeded. A seller who is offline gets paid the next time they're online (checked every minute and whenever they open the market). Refunds to an offline buyer work the same way.
- A restart in the middle of a sale is finished on the next start: a sale the buyer already paid for is completed or refunded, and a listing that was only claimed goes back on the market. Active listings survive restarts.
Exports
These are server side and read the database, so call them from a thread or an event handler.
-- Open the market screen for a player (for a dealer ped, a target zone or a phone app)
exports.vehicle_market:OpenMarket(source)
-- Current value of a car: { value, buyback, base, km, days, condition } or nil
local v = exports.vehicle_market:GetValue('84KTR201')
-- Active listings, newest first
local listings = exports.vehicle_market:GetListings()
Security
Every action runs on the server and is checked there. The client only sends a listing id, a plate or an asking
price. The server looks up the listing, the owner, the value and the price bounds itself, rate limits every player
action, and checks vehiclemarket.admin on the server for the admin view. Two buyers clicking at the same moment
can't both get the car, because the database hands the listing to exactly one of them (even with two servers on
one database). Also, a car can only have one open listing, which the database enforces as well.
Troubleshooting
- Nothing happens on start, or "couldn't find fxmanifest.lua": the folder is nested one level too deep. You
need
resources/vehicle_market/fxmanifest.lua. No framework finished starting within 30s: the message lists what it saw. Make sure your core is started, or setConfig.Framework.Database setup failed: oxmysql isn't connected, or the database user can't create tables. The message includes the database's own error.market open on esx, 0 models priced: thevehiclestable is missing or empty. SetConfig.EsxShopTableto your shop's table, or add prices toConfig.Prices.- A car shows "Park the car in a garage first" while it's parked: your garage doesn't set
stored(ESX) orstate(QBCore, Qbox) to 1 when it parks a car. Check with your garage's author which column it uses. - A car shows "That car is out on the street": a vehicle with that plate exists in the world, often a copy the garage spawned and never deleted. Delete the entity or restart, then try again.
- Mileage stays at 0: the car isn't player owned under the same plate (the market only records owned cars),
or
Config.Mileage.enabledis false.
Escrow
config.lua, locales/, server/bridge.lua and sql/install.sql are left open, so you can change settings,
translate the text, adapt the bridge to a forked core or garage, and read the tables.
Support
Open a ticket in the Discord with your framework, garage, the start line from the server console and any red lines below it.