Overview
GTA's map is covered in vending machines nobody can use. This resource turns every one of them into a working shop — and then gives that shop a reason to exist in your economy.
A machine holds a finite amount of stock. Players buy from it, it empties, and it stays empty until either the slow automatic trickle refills it or a player driving the restock route loads it by hand. Somebody with a crowbar can force it open for the cash float, which puts a call out to your police.
Nothing about this needs you to place machines. The resource keys off the prop models already in the map, so the soda machine outside the Pillbox ER and the one in the airport terminal are both live the moment you start the resource — and they keep their stock separately.
It runs on qb-core, qbx_core, es_extended or standalone, and every integration is optional and detected at runtime.
Requirements
None. The resource starts and works on a bare server. Everything below is optional; each is detected at runtime and the resource degrades cleanly without it.
| Optional | What it adds | Without it |
|---|---|---|
qb-core / qbx_core / es_extended | Money, jobs, inventory | Standalone: machines vend, nothing is charged |
ox_inventory | Item handling, item images, weight checks | Falls back to your framework's inventory |
ox_target or qb-target | Third-eye interaction | A drawn marker with an E prompt |
ox_lib | Context menu, quantity dialog, skill check, progress bar | qb-menu, then a built-in NUI shop |
qb-menu | Menu when ox_lib is absent | The built-in NUI shop |
oxmysql | Stock that survives a restart | Stock works, but resets on restart |
qb-management / qb-banking / Renewed-Banking / esx_addonaccount | Society revenue | Takings are not routed anywhere |
sals_kewldispatch / ps-dispatch | Police alerts on break-ins | On-duty officers get a notification and a blip |
wasabi_ambulance | Accurate downed-player checks | Falls back to framework metadata, then entity health |
Do not edit fxmanifest.lua. Version 1.x shipped with
instructions to comment out dependency lines depending on what you ran. That step
is gone — there is no dependencies block to edit, because nothing is
a hard requirement.
Installation
- Extract the folder into your server's
resources/directory. The folder must be namedsals_kewlvending. - Add the start line to
server.cfg:ensure sals_kewlvending - Grant configurator access to whoever should have it:
add_ace group.admin sals_kewlvending.config allow - Open
config.luaand set your machines, items and prices.
Database
Nothing to import. With oxmysql running, the sals_kewlvending_stock
table is created on first start. Without oxmysql, stock is kept in memory for the
uptime of the server and machines refill on restart.
Items your inventory needs
The resource sells whatever item names you put in config.lua. On the
shipped defaults that is sprunk, ecola, water,
coffee, latte, cigarette, joint,
the sweet_* snacks, and vending_crate for the restock job.
Upgrading from 1.x: the cigarette machine now sells cigarette, not
cigar. Add the item or change it back in your config.
How machines work
This is the one concept worth reading before you edit anything, because the config reads strangely until you have it.
A machine in config.lua is not one prop in the
world — it is a kind of machine. One entry covers every soda machine on the
map and lists what that kind sells.
Stock, however, is tracked per physical machine. The one outside the Pillbox ER and the one at the airport are the same kind, sell the same Sprunk, and run out independently.
A physical machine is identified by its kind's Id plus its rounded
world position. That is why stock survives a restart without you placing anything:
a map prop does not move, so its position is its identity.
Changing a machine's Id after players have been using it resets
that machine's stock, because the identity changed. Pick an Id and
leave it alone.
Machines & items
Machines live in Config.VendingMachines. A full entry:
{
Id = 'soda', -- unique, and permanent
Label = 'Soda Machine',
Icon = 'fa-solid fa-bottle-water',
Payment = 'either', -- 'cash', 'bank' or 'either'
Society = 'vendingco', -- optional; see Pricing & payment
PropTypes = {
'prop_vend_soda_01',
'prop_vend_soda_02',
},
Items = {
{ Item = 'sprunk', Label = 'Sprunk', Price = 5 },
{ Item = 'ecola', Label = 'eCola', Price = 7 },
},
},
| Key | Meaning |
|---|---|
Id | Unique, permanent. Half of how a physical machine is identified. |
Label | What players see in the menu and on the target. |
Icon | FontAwesome class for the target option. Optional. |
Payment | cash, bank, or either to let the player choose. Falls back to Config.Purchase.DefaultPayment. |
Society | Account that receives this machine's takings. Optional. |
PropTypes | Prop model names this kind covers. A prop may belong to only one machine. |
Items | What it sells. Price must be a whole number. |
Any prop model works, not only vending props — point a machine at a fridge, a crate or a market stall and it behaves the same way.
Stock
Config.Stock = {
Enabled = true,
Persist = true, -- needs oxmysql
Default = 20, -- units each slot holds when full
RestockMinutes = 45, -- how often idle machines trickle back up
RestockAmount = 5, -- units added per slot per trickle
ShowCount = true, -- show "12 left" in the menu
}
A machine the server has never seen is full by definition, so there is nothing to seed and no startup cost for a map full of props. Machines enter the database the first time somebody buys from one or robs it.
Set Enabled = false and machines are bottomless, which is how
version 1.x behaved.
The passive trickle exists so a server with nobody driving the restock route
does not end up with a map of empty boxes. Turn it down or set
RestockAmount = 0 if you want the restock job to be the only way
machines refill.
Pricing & payment
Location pricing
A machine inside the airport terminal charges what an airport charges. The first zone a machine falls inside wins; machines outside every zone pay the base price.
Config.Pricing = {
Enabled = true,
Zones = {
{ Label = 'Los Santos International', Coords = vec3(-1037.0, -2737.0, 20.0), Radius = 400.0, Multiplier = 1.60 },
{ Label = 'Bolingbroke Penitentiary', Coords = vec3(1845.0, 2585.0, 45.0), Radius = 350.0, Multiplier = 1.75 },
{ Label = 'Sandy Shores', Coords = vec3(1961.0, 3740.0, 32.0), Radius = 500.0, Multiplier = 0.85 },
},
}
The adjusted price is rounded to a whole dollar and shown in the menu, so a player always sees what they are about to be charged.
Cash or card
Set Payment per machine. With either, the player is
asked at checkout (with ox_lib) or charged cash (without it).
Society revenue
Config.Ownership = {
Enabled = false,
Share = 1.0, -- 0.0-1.0 of each sale that reaches the society
}
Turn this on and give a machine a Society, and that machine's
takings land in the business account instead of vanishing. Works with
qb-management, qb-banking, Renewed-Banking and esx_addonaccount. With no banking
resource installed the sale still stands; the money simply is not routed.
Break-ins
A player carrying a lockpick, screwdriver or crowbar can force a machine open for the cash float and whatever stock is still inside.
Config.Robbery = {
Enabled = true,
RequiredItems = { 'lockpick', 'screwdriver', 'crowbar' }, -- any one
ConsumeItem = false, -- take the tool on a failed attempt
Duration = 12000,
Cooldown = 60, -- minutes before the same machine can be hit again
CashMin = 40,
CashMax = 220,
ItemChance = 60, -- % chance to also spill stock
MaxItems = 5,
PoliceAlert = true,
MinimumPolice = 0, -- officers on duty required; 0 disables the check
AlertChance = 85, -- % chance the alarm reaches dispatch
PoliceJobs = { 'police', 'sheriff', 'statepolice', 'bcso' },
SkillCheck = { 'easy', 'easy', 'medium' },
}
With ox_lib installed the break-in is a skill check; without it, a timed progress bar. The alarm fires when the machine is forced, not when it is emptied, so police have a chance to arrive before the thief is finished.
AlertChance below 100 means some break-ins go unreported, which
keeps players from treating the alarm as a certainty. The cooldown applies to the
attempt whether it succeeded or failed, so a failed try cannot be retried instantly.
Break-ins ship enabled. Set Enabled = false if you
do not want them on your server.
The restock job
A paid loop for players: collect crates from the depot, drive to machines that are actually low, and refill them.
Config.Restock = {
Enabled = true,
Job = nil, -- nil = anyone may do it; or 'trucker'
Depot = vec3(-424.0, -2790.0, 6.0),
CrateItem = 'vending_crate',
CrateCapacity = 40, -- units one crate refills
CratePrice = 0, -- charge per crate, 0 for free
PayPerUnit = 6,
PayAccount = 'bank',
MaxCrates = 3,
LowStockPct = 50, -- machines at or below this show on the route
RouteBlips = 6,
}
A driver runs /vendingroute to mark the nearest machines that need
something, drives to one, and uses the restock option on it. Pay is calculated from
the units actually loaded, after the refill succeeds.
Only machines the server has seen can be low — an untouched machine is full by definition. That falls out of how stock works, and it means the route only ever sends a driver somewhere worth going.
Eating & drinking
Vended items restore hunger and thirst, and coffee takes the edge off stress.
Only the items listed in Config.Consumables.Items are registered —
leave an item out and whatever food resource you already run keeps ownership of it.
Config.Consumables = {
Enabled = true,
Items = {
['sprunk'] = { thirst = 24, hunger = 0, drink = true },
['coffee'] = { thirst = 18, hunger = 0, drink = true, stress = -6 },
['sweet_cake_a'] = { thirst = 0, hunger = 26 },
},
}
Values are percentage points added to the player's status.
With ox_inventory, item use is driven by the item definition,
not from here. Point your definitions at this resource in
ox_inventory/data/items.lua:
['sprunk'] = {
label = 'Sprunk',
weight = 200,
client = { status = { thirst = 200000 } },
server = { export = 'sals_kewlvending.useConsumable' },
},
On qb-core and ESX the items are registered as usable automatically.
The configurator
/vendingconfig opens an in-game editor for your machine list —
machines, prop models, items and prices — with changes applied live to everyone
online.
Access
Access is an ACE permission, checked on the server:
add_ace group.admin sals_kewlvending.config allow
If you would rather name individual people, list their identifiers in
Config.Permissions.AllowedIdentifiers. It is an OR with the ACE check
and ships empty on purpose.
How changes are stored
The configurator never touches config.lua. It
validates what you typed and writes it to a separate
config_overrides.json that layers on top at start. Your comments and
formatting survive, and deleting that one file — or running
/vendingreset — puts you back exactly where you began.
A save is rejected as a whole rather than repaired. Prices must be whole
numbers, machine Ids must be unique, and prop and item names are
restricted to sane characters. If a save is refused the editor tells you which
entry was the problem.
Commands
| Command | Who | What |
|---|---|---|
/vendingconfig | ACE sals_kewlvending.config | Open the configurator |
/vendingreload | ACE / console | Reload configuration from disk |
/vendingreset | ACE / console | Discard configurator changes, return to config.lua |
/vendingstock | ACE / console | List tracked machines and their fill level |
/vendingstock reset | ACE / console | Refill every machine |
/vendingroute | Anyone, or the restock job | Mark machines that need restocking |
Audit logging
Config.Logging = {
Enabled = true,
Console = true,
Webhook = '', -- a secret; keep it out of any repository
Events = {
purchase = false, -- every sale is noisy; on for economy audits
robbery = true,
restock = true,
config = true, -- who changed the configurator, and when
rejected = true, -- refused purchases: where cheating shows up
},
}
Logs go to the server console always, and to a Discord webhook when one is set. A webhook that is down never costs a player their money — logging is fire and forget and cannot block a sale.
The rejected channel is the useful one for spotting trouble: it
records purchases the server refused, which is what an automated client trying its
luck looks like from the outside.
Upgrading from 1.x
Your 1.x config.lua will not carry over. The
machine format changed. Start from the new shipped config and re-enter your
prices rather than trying to patch the old one.
| 1.x | 2.0.0 |
|---|---|
propTypes, items, item, label, price | PropTypes, Items, Item, Label, Price — and every machine needs a unique Id |
Config.AllowedIDs | add_ace group.admin sals_kewlvending.config allow |
Config.Debug = true | Config.Debug = { Enabled = false, Prefix = '...' } (a plain boolean still works) |
/openvendingconfig | /vendingconfig |
cigar | cigarette |
Edit fxmanifest.lua to match your framework | Nothing to edit |
The sals_kewlvending:purchase event signature changed and no longer
accepts a price. Anything you built against it must be updated.
Default snack prices were spread out rather than left flat at $15 — lollipops $12, candy $14, cookies $13, chocolate $16, cake $18. Your own prices are unaffected if you re-enter them.
What the server verifies
Worth stating plainly, because it is the difference between a vending script and a hole in your economy.
Every transaction is decided by the server. The client may say "I am at this kind of machine, at roughly this spot, and I want slot 3, four of them." Everything that matters is answered from your config:
- the player's real position is within reach of the machine they claim;
- the item is one that machine actually sells;
- the price is the one in your config, adjusted by your location zones;
- the money is really deducted before the item is handed over, and refunded if the inventory refuses it;
- the stock really depletes;
- and the whole thing is rate limited per player.
What the server cannot do is confirm that a vending prop exists at a given spot — map props are client-side and a FiveM server has no handle on them. A determined cheater can therefore claim to be standing at a machine while standing in a field. They still cannot change what is sold, what it costs, or get something for nothing.
Version 1.x was not server-authoritative: the purchase event accepted an item name and a price from the client. If you are running 1.x, upgrading is the fix.
Troubleshooting
Nothing happens when I look at a vending machine
Check the console on start for the targeting ready line with debug
on. If it says marker, you have no targeting resource and should walk
up and press E. If the prop is not one of yours, add its
model name to a machine's PropTypes.
Machines are always full
Config.Stock.Enabled is false, or RestockAmount and
RestockMinutes are refilling them faster than players empty them.
Stock resets every restart
oxmysql is not running, or Config.Stock.Persist is false. The
console says which on start.
Players are charged but get nothing
They should not be — a failed inventory add is refunded, stock included, and
logged under rejected. If you see this, check that log channel and send
it to support.
The restock route says nothing needs restocking
Only machines the server has seen can be low. Until somebody buys from a machine it is full by definition, so a fresh server has an empty route. This is expected.
The configurator says access denied
The ACE is missing from server.cfg, or you are not in the group it
was granted to. Confirm with add_ace group.admin sals_kewlvending.config
allow and check the player is actually in group.admin.
Consumables do nothing when used
With ox_inventory, item use comes from the item definition — point it at
server = { export = 'sals_kewlvending.useConsumable' }. Without
ox_inventory, check the item name in Config.Consumables.Items matches
your inventory exactly.
Support
Support, release news and coupon codes are on Discord. Purchases are handled by Tebex, who own the checkout, billing support and refunds.
The resource ships README.md and a full CHANGELOG.md
inside the download.