esx_shops
esx_shops adds 24/7, liquor and LTD stores with a cart-based NUI to ESX Legacy. Covers setup, config keys, tax, shop zones, exports and pitfalls.
esx_shops is the official store resource for ESX Legacy. It places 24/7, Rob's Liquor and LTD Gasoline stores on the map and opens a Svelte-based shopping cart UI where players pick items, choose cash or bank and pay. Use it as your general store system or as a template for your own item shops.
Overview
| Resource | esx_shops (version 2.0.0) |
| Side | Client + server, NUI (web/dist) |
| Dependencies | esx_lib, es_extended, oxmysql |
| Optional | esx_addonaccount (tax collection), ox_inventory |
| SQL file | None |
| Config files | shared/config/main.lua, shared/config/shops.lua |
How a purchase works:
- The player walks into a marker and presses the interaction key. The client asks the server for the player's tax rate and opens the NUI.
- On checkout the client sends the cart through the
esx_shops:purchaseItemscallback. - The server rate-limits the request, checks the zone, the distance to the shop, the payment method, every item name, quantity and price against
Config.Zones, and recomputes the total. The client total must match the server total withinConfig.PriceTolerance. - The server checks inventory weight, deducts money and adds the items. If adding an item fails, already added items are removed and the money is refunded.
- The tax part of the price is deposited into
Config.TaxSocietyAccountwhen tax collection is on.
Installation
Add the resource
Place esx_shops in your resources folder. The repository ships a prebuilt UI in web/dist, so you only need Node.js if you change the Svelte source (cd web && npm install && npm run build).
Ensure it after its dependencies
ensure oxmysql
ensure esx_lib
ensure es_extended
ensure esx_addonaccount
ensure esx_shopsPrepare the tax account (optional)
With Config.EnableTaxCollection = true the tax goes into the shared addon account named in Config.TaxSocietyAccount (society_banker by default). Create it if it does not exist:
INSERT IGNORE INTO `addon_account` (`name`, `label`, `shared`) VALUES ('society_banker', 'Bank', 1);With Config.Inventory = 'esx' the resource registers every configured shop item via ESX.AddItems on startup (label, weight or Config.DefaultItemWeight). You still want proper rows in your items table for items you use elsewhere.
Configuration
Main config (shared/config/main.lua)
| Key | Default | Description |
|---|---|---|
Config.Debug | false | Prints validation warnings and errors to the console. |
Config.Inventory | 'esx' | 'esx', 'ox_inventory' (or 'ox') or 'auto'. Falls back to ESX if ox_inventory is configured but not started. |
Config.DefaultItemWeight | 1 | Weight used when a shop item has no weight. |
Config.DefaultImagePath | "nui://esx_shops/web/images" | Base path for item images. Set to nil or '' to disable auto paths. |
Config.DefaultImageFormat | "png" | File extension for auto-generated image paths. |
Config.TaxRate | 0.19 | Tax share included in every price (19 %). |
Config.EnableTaxCollection | true | Deposit the tax into the society account. false only shows it in the UI. |
Config.TaxSocietyAccount | 'society_banker' | Shared addon account that receives tax. |
Config.EnableTaxExemptions | true | Lets jobs in TaxExemptJobs pay the net price. |
Config.TaxExemptJobs | { 'police', 'ambulance' } | Job names that do not pay tax. |
Config.DrawDistance | 7.5 | Marker draw distance in meters. |
Config.MarkerSize | {x = 1.1, y = 0.7, z = 1.1} | Marker scale. |
Config.MarkerType | 29 | Marker type. |
Config.MarkerColor | {r = 50, g = 200, b = 50, a = 200} | Marker color. |
Config.MovementThreshold | 5.0 | Meters the player must move before nearby shops are recalculated. |
Config.SleepNear / SleepMedium / SleepFar | 0 / 500 / 1500 | Marker thread sleep in ms (under 50 m, 50-100 m, further). |
Config.PurchaseCooldownMs | 500 | Refill interval of the purchase rate limiter. |
Config.CooldownExpiryMs | 10000 | Idle time after which limiter entries are cleaned up. |
Config.PurchaseRateLimitCapacity | 3 | Token bucket size (burst of purchases allowed). |
Config.PurchaseRateLimitRefill | 1 | Tokens added per interval. |
Config.PurchaseRateLimitIntervalMs | Config.PurchaseCooldownMs | Interval for token refill. |
Config.MaxQuantityPerItem | 999 | Maximum quantity per cart line. |
Config.MaxCartLines | 25 | Maximum distinct items per purchase. |
Config.PriceTolerance | 0.001 | Allowed difference between client and server total. |
Shop zones (shared/config/shops.lua)
Config.Zones ships with three zones: TwentyFourSeven, RobsLiquor and LTDgasoline. Each zone looks like this:
Config.Zones.Hardware = {
Items = {
{ name = 'repairkit', label = 'Repair Kit', price = 250, category = 'tools', weight = 2 },
{ name = 'bandage', label = 'Bandage', price = 40, category = 'medical' },
},
Categories = {
{ id = 'tools', label = 'Tools', icon = 'fa-solid fa-wrench' },
{ id = 'medical', label = 'Medical', icon = 'fa-solid fa-kit-medical' },
},
Pos = {
vector3(2748.0, 3473.0, 55.7),
},
Size = 0.8, -- blip scale
Type = 59, -- blip sprite
Color = 25, -- blip color
ShowBlip = true,
ShowMarker = true,
}| Field | Type | Description |
|---|---|---|
Items[].name | string | Item name in your inventory. |
Items[].label | string | Name shown in the UI. |
Items[].price | number | Gross price per unit (tax included). Must be greater than 0. |
Items[].category | string? | Category id for filtering. |
Items[].weight | number? | Weight used when the item is auto-registered for the ESX inventory. |
Items[].image | string? | Custom image URL. Otherwise DefaultImagePath/<name>.<DefaultImageFormat>. |
Items[].limit | number? | Passed to the UI as a per-transaction limit. The server only enforces Config.MaxQuantityPerItem. |
Categories | table? | { id, label, icon } entries, icons are Font Awesome classes. |
Pos | vector3[] | Every location of this shop. |
Size, Type, Color | number | Blip scale, sprite and color. |
ShowBlip, ShowMarker | boolean | Toggle blip and 3D marker. |
Tax math
Prices include tax. For a 100 item at a TaxRate of 0.19, a normal player pays 100 and 15.97 goes to the society account. A tax-exempt player pays 84.03 and no tax is collected.
The UI colors follow the ESX theme convars (esx:ui:*) via xLib.colors.getESXTheme.
Developer API
Server export IsItemInShop
exports.esx_shops:IsItemInShop(itemName, zone)| Name | Type | Description |
|---|---|---|
itemName | string | Item name to look up. |
zone | string | Key in Config.Zones, e.g. 'TwentyFourSeven'. |
Returns true when the item is sold in that zone, otherwise false.
if exports.esx_shops:IsItemInShop('lockpick', 'TwentyFourSeven') then
print('Lockpicks are sold in 24/7 stores')
endShared exports GetShopItems / GetShopCategories
Both are available on client and server and are marked as legacy. They return the Items or Categories table of a zone, or {} if the zone does not exist.
local items = exports.esx_shops:GetShopItems('RobsLiquor')Client exports
| Export | Returns |
|---|---|
exports.esx_shops:IsShopOpen() | true while the shop UI is open. |
exports.esx_shops:GetCurrentShop() | The zone name of the open shop, or nil. |
Client event esx_shops:forceOpenShop
Opens the shop UI for a zone if it exists in Config.Zones.
TriggerClientEvent('esx_shops:forceOpenShop', playerId, 'TwentyFourSeven')Purchases still require the player to stand within 5 meters of one of that zone's Pos entries, so this is only useful near a real shop location (for example from a target or ped script).
Callbacks and network events
| Name | Type | Notes |
|---|---|---|
esx_shops:purchaseItems | Server callback | Used by the NUI. Arguments: purchaseData ({ items, total, paymentMethod }) and zone. |
esx_shops:getTaxRate | Server callback | Returns the player's tax rate and an optional exemption message. |
esx_shops:legacyPurchase | Server net event | Compatibility event: (zone, items, paymentMethod). Answers with the client event esx_shops:legacyPurchaseResponse(success, message). |
esx_shops:openShop | Net event | Legacy placeholder. It only validates the argument and does nothing else. |
Client-triggerable
esx_shops:purchaseItems and esx_shops:legacyPurchase can be called by any client. Both go through the same server pipeline (rate limit, distance check, server-side prices, inventory space, refund on failure), so clients cannot set prices. Keep Config.Zones the only source of prices and do not add your own purchase events that skip ProcessPurchase.
Common issues
- "Not enough money" although the player has cash. The payment method chosen in the UI decides between
moneyandbank. - Tax is not deposited.
Config.TaxSocietyAccountdoes not exist inaddon_account, oresx_addonaccountis not started. EnableConfig.Debugto see the warning. - Item not added / inventory full. With the ESX inventory, every item must be registered and fit the player's max weight. With ox_inventory,
CanCarryItemmust succeed. - Images missing. Put a PNG named after the item into
web/imagesor setimageon the item. - Shop opens but purchases fail far from the marker. See the distance check above.
Related
Frequently asked questions
Independent documentation
Last updated on
esx_service
esx_service is the on/off duty system for ESX Legacy jobs. It limits how many players are on duty, tracks counts in GlobalState and notifies colleagues.
esx_society
esx_society adds shared job accounts, the boss menu, hiring, salaries, grade labels, uniforms and money washing to ESX Legacy. Full API and config reference.