Why build a custom vending machine?
Our server needed vending machines that weren't just static item dispensers. We wanted:
- Job-specific product lists (each business manages their own machine)
- Stock management by the job's employees
- Sales revenue tracking with cash-out by a boss
- Discord webhook notifications when items sell out
- Admin panel for placing and configuring machines in-game
The result: lokat_vendex — a full vending machine system built on QBCore with ox_inventory support.
How it works
The system has three layers of users:
Customers approach a machine, browse available products by job/shop, and purchase items with cash or bank funds.
Managers (job members) deposit stock from their inventory into the machine, set prices, and withdraw accumulated sales revenue.
Admins place new machines in-game, assign which jobs operate each machine, and adjust configurations.
Key features
Job-based product separation
Each machine can serve multiple jobs. Products are stored per machine_id + job + item, so a machine shared between two businesses shows each business only their own products.
-- Products are scoped by machine and job
SELECT * FROM vm_products WHERE machine_id = ? AND job = ?
Race-safe stock deduction
When a player purchases an item, stock is decremented using a conditional UPDATE that only succeeds if sufficient stock exists. This prevents two simultaneous purchases from taking more stock than available.
UPDATE vm_products
SET stock = stock - ?
WHERE machine_id = ? AND job = ? AND item = ? AND stock >= ?
If the update affects 0 rows, the purchase is rejected rather than allowing negative stock.
Partial refund on cart failure
When buying multiple items in one cart, each item is processed individually. If one item fails (stock race, inventory full), only that item's cost is refunded — the rest of the cart succeeds. The player always ends up with exactly what they paid for.
Event item purchase limits
Items marked with the event category have a per-player purchase cap, configurable in the server config. The cap resets on server restart. This is useful for limited-time event items.
Config.EventItemLimit = 20 -- max purchases per player per restart
Discord webhook on sell-out
When any item's stock hits zero (from a purchase or manual withdrawal), the script sends a webhook notification to the relevant Discord channel. Each job can have its own webhook URL configured separately.
-- Example config structure (use your own webhook URLs)
Config.WebhookByJob = {
your_job_name = {
{ url = "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN" }
},
}
The webhook payload includes the job name, item name, and who triggered the sell-out — so the right team gets notified automatically.
In-game machine placement
Admins can place machines without touching config files. The placement flow works like this:
- Open the admin panel with a command
- Select a position using an in-game preview prop
- Assign jobs that will operate the machine
- Confirm — the machine is saved to the database and instantly synced to all players
The preview prop follows the admin's character in real-time, showing the interaction radius as a marker. Rotation and radius are adjustable before confirming.
Dual target support
Works with both qb-target and ox_target. Configured with a single line:
Config.Target = 'qb-target' -- or 'ox_target'
Cash or bank payment
Players can pay with cash, bank, or auto mode (cash first, bank fallback). Configurable per server:
Config.Payment = {
default = 'auto', -- 'cash' | 'bank' | 'auto'
payoutMethod = 'bank' -- how managers receive their sales payout
}
Database schema overview
Four tables handle the full lifecycle:
| Table | Purpose |
|---|---|
vm_machines |
Machine locations, labels, assigned jobs |
vm_products |
Per-machine, per-job inventory and pricing |
vm_balances |
Accumulated sales revenue per machine per job |
vm_ledger |
Full audit log of every transaction |
The ledger table records every sale, deposit, withdrawal, and adjustment with actor name and timestamp — useful for dispute resolution and monitoring.
Security design
All permission checks run server-side. The client never decides whether an action is allowed — it only sends requests. The server validates:
- Admin operations require ACE permission
lokat_vendex.adminor QBCore admin/god flags - Manager operations (stock, payout) check that the player's current job matches the machine's assigned jobs
- Boss-only operations (collect all sales) additionally check
job.isboss - A debounce cooldown blocks rapid duplicate submissions
Client-side canInteract checks are for UI only — they hide options from unauthorized players but are never trusted by the server.
Next up
Vol.6 — wrapping up the series with a look at how all these scripts fit together as a server-management ecosystem, and what I'd do differently if building from scratch today.
The AI behind this
This script — like everything in this series — was built with AI as my coding partner.
I don't write Lua myself. I design the systems, think through the edge cases,
and the AI handles implementation.
But getting here wasn't straightforward. I went through ChatGPT, Gemini,
and a few others before landing on Claude Code as my go-to.
Each had its strengths, but Claude Code was the one that actually stuck.
That story deserves its own post — coming soon.
Questions about the vending machine system? Drop a comment.
Questions about the vending machine system? Drop a comment.
Top comments (0)