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.stored on ESX (esx_garage and most others), player_vehicles.state on 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

  1. Unzip so that resources/vehicle_market/fxmanifest.lua exists (not resources/vehicle_market-v1.0.0/vehicle_market/..., which Windows "Extract All" creates), then add ensure vehicle_market after ensure oxmysql. Start order against your framework doesn't matter, it waits up to 30 seconds and prints what it found, for example market open on qbcore, 214 models priced.
  2. Give staff the admin view in server.cfg:
    add_ace group.admin vehiclemarket.admin allow
    
  3. Start the server. The four vehicle_market_* tables create themselves.
  4. Players open the market with /market. If you'd rather open it from a dealer ped or a target zone, call exports.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 set Config.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: the vehicles table is missing or empty. Set Config.EsxShopTable to your shop's table, or add prices to Config.Prices.
  • A car shows "Park the car in a garage first" while it's parked: your garage doesn't set stored (ESX) or state (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.enabled is 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.