DEV Community

Tack k
Tack k

Posted on

Building a Full-Featured Vending Machine System for QBCore — Stock, Sales, and Discord Alerts

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 = ?
Enter fullscreen mode Exit fullscreen mode

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 >= ?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }
    },
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open the admin panel with a command
  2. Select a position using an in-game preview prop
  3. Assign jobs that will operate the machine
  4. 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'
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.admin or 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)