Tutorial2026-05-18

FiveM Vending Machine & Shop Script

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

Why Vending Machines Add Depth to Your Server

Vending machines and automated shop systems serve a crucial role in any roleplay economy. They provide 24/7 access to essential items like food, water, cigarettes, and basic supplies without requiring a store clerk to be online. Beyond convenience, they create natural gathering points where players congregate, leading to spontaneous roleplay encounters at gas stations, hospitals, and office buildings. A well-implemented vending machine system goes far beyond a simple buy menu. It includes realistic purchase animations, stock management that creates scarcity, dynamic pricing that responds to supply and demand, different machine types that sell different product categories, robbery mechanics for criminal roleplay, and restocking jobs that give players meaningful employment. This tutorial covers building a complete vending and shop system from prop detection through inventory integration and economy management.

Detecting and Configuring Vending Machine Props

GTA V's world is filled with vending machine props that you can turn into interactive objects. The key prop models include prop_vend_soda_01 and prop_vend_soda_02 for soda machines, prop_vend_water_01 for water dispensers, prop_vend_coffe_01 for coffee machines, and prop_vend_snak_01 for snack machines. Rather than manually placing interaction points at every machine location across the map, use a radius-based detection system that scans for these prop models near the player. When a player approaches any matching prop within interaction range, the system activates a prompt or target zone. This approach automatically works with every instance of these props across the entire map without manual configuration, and it even picks up custom MLO interiors that include these models. Define different product catalogs for each prop type so soda machines only sell drinks, snack machines sell food items, and coffee machines sell coffee variants.

-- shared/config.lua
Config = {}

Config.InteractDistance = 1.5
Config.UseAnimation     = true
Config.AnimDuration     = 3000 -- ms

Config.Machines = {
    soda = {
        models = {
            `prop_vend_soda_01`,
            `prop_vend_soda_02`,
        },
        label = 'Soda Machine',
        items = {
            {name = 'cola',       label = 'Cola',        price = 5,  stock = 20},
            {name = 'sprunk',     label = 'Sprunk',      price = 5,  stock = 20},
            {name = 'ecola',      label = 'E-Cola',      price = 6,  stock = 15},
            {name = 'energydrink',label = 'Energy Drink', price = 8,  stock = 10},
        },
    },
    snack = {
        models = {`prop_vend_snak_01`},
        label = 'Snack Machine',
        items = {
            {name = 'chips',      label = 'Chips',       price = 4,  stock = 25},
            {name = 'candy',      label = 'Candy Bar',   price = 3,  stock = 30},
            {name = 'sandwich',   label = 'Sandwich',    price = 7,  stock = 15},
            {name = 'granola',    label = 'Granola Bar',  price = 5,  stock = 20},
        },
    },
    coffee = {
        models = {`prop_vend_coffe_01`},
        label = 'Coffee Machine',
        items = {
            {name = 'coffee',     label = 'Black Coffee', price = 4,  stock = 30},
            {name = 'latte',      label = 'Latte',       price = 6,  stock = 20},
            {name = 'espresso',   label = 'Espresso',    price = 5,  stock = 25},
        },
    },
    water = {
        models = {`prop_vend_water_01`},
        label = 'Water Cooler',
        items = {
            {name = 'water',      label = 'Water Bottle', price = 3,  stock = 40},
        },
    },
}

Client-Side Interaction and Animation System

When a player approaches a vending machine and presses the interaction key, several things happen in sequence to create an immersive purchase experience. First, the player's movement is restricted to prevent them from walking away mid-animation. Then the player character turns to face the machine using TaskTurnPedToFaceEntity. A purchase animation plays using the MINI@SPRUNK animation dictionary for soda machines or ANIM@AM_HOLD_UP@MALE for general interactions. The machine itself plays a sound effect using PlaySoundFromEntity to simulate the mechanical dispensing noise. After the animation completes, the client sends a purchase request to the server, which validates the transaction and adds the item to the player's inventory. If the machine has a coin slot interaction, add a small prop attachment to the player's hand during the animation using AttachEntityToEntity for extra visual fidelity. The entire sequence should take about 3 seconds, long enough to feel realistic but short enough to not frustrate players.

-- client/interaction.lua
local isBuying = false

function PurchaseFromMachine(machineEntity, machineType, itemIndex)
    if isBuying then return end
    isBuying = true

    local ped = PlayerPedId()
    local machineCoords = GetEntityCoords(machineEntity)

    -- Face the machine
    TaskTurnPedToFaceEntity(ped, machineEntity, 1000)
    Wait(1000)

    -- Play animation
    if Config.UseAnimation then
        RequestAnimDict('mini@sprunk')
        while not HasAnimDictLoaded('mini@sprunk') do Wait(10) end

        TaskPlayAnim(ped, 'mini@sprunk', 'plyr_buy_drink_pt1',
            8.0, -8.0, 2000, 0, 0, false, false, false)
        Wait(1500)

        TaskPlayAnim(ped, 'mini@sprunk', 'plyr_buy_drink_pt2',
            8.0, -8.0, 1500, 0, 0, false, false, false)

        -- Machine sound effect
        PlaySoundFromEntity(-1, 'vending_machine_purchase',
            machineEntity, 'dlc_vw_table_games', false, 0)
        Wait(1500)
    end

    -- Request purchase from server
    TriggerServerEvent('vendingmachine:purchase', machineType, itemIndex)

    ClearPedTasks(ped)
    isBuying = false
end

Server-Side Stock Management and Economy

Stock management transforms vending machines from unlimited item dispensers into dynamic economic elements. Each machine location tracks its own inventory independently. When a machine runs out of a particular item, players see it marked as sold out in the purchase menu. Stock levels persist in the database across server restarts. A restocking job allows players to work as delivery drivers who pick up supplies from a warehouse and drive them to empty machines around the city. This creates an entire economic loop: suppliers restock machines, machines sell to consumers, revenue flows back to the machine owner, and the delivery driver earns wages. Dynamic pricing adds another layer where popular items gradually increase in price as stock decreases, and prices reset when the machine is restocked. This naturally balances consumption patterns and creates urgency when a machine is running low on a desirable item.

-- server/stock.lua
local machineStock = {}

function GetMachineKey(machineType, coords)
    return machineType .. ':' ..
        math.floor(coords.x) .. ':' ..
        math.floor(coords.y) .. ':' ..
        math.floor(coords.z)
end

function GetStock(machineKey, itemIndex)
    if not machineStock[machineKey] then
        -- Load from database or initialize defaults
        machineStock[machineKey] = {}
    end
    return machineStock[machineKey][itemIndex] or
        Config.Machines[machineKey:match('^(%w+):')].items[itemIndex].stock
end

RegisterNetEvent('vendingmachine:purchase', function(machineType, itemIndex)
    local src = source
    local config = Config.Machines[machineType]
    if not config or not config.items[itemIndex] then return end

    local item = config.items[itemIndex]
    local machineKey = machineType -- simplified; use coords in production

    -- Check stock
    local stock = GetStock(machineKey, itemIndex)
    if stock <= 0 then
        TriggerClientEvent('ox_lib:notify', src, {
            title = 'Sold Out',
            description = item.label .. ' is out of stock',
            type = 'error'
        })
        return
    end

    -- Calculate dynamic price
    local basePrice = item.price
    local stockRatio = stock / item.stock
    local dynamicPrice = math.ceil(basePrice * (1 + (1 - stockRatio) * 0.5))

    -- Deduct money
    local paid = exports['framework']:RemoveMoney(src, dynamicPrice, 'cash')
    if not paid then
        TriggerClientEvent('ox_lib:notify', src, {
            title = 'No Cash',
            description = 'You need $' .. dynamicPrice,
            type = 'error'
        })
        return
    end

    -- Add item to inventory
    local added = exports['ox_inventory']:AddItem(src, item.name, 1)
    if not added then
        exports['framework']:AddMoney(src, dynamicPrice, 'cash')
        return
    end

    -- Decrease stock
    machineStock[machineKey] = machineStock[machineKey] or {}
    machineStock[machineKey][itemIndex] = stock - 1

    TriggerClientEvent('ox_lib:notify', src, {
        title = 'Purchased',
        description = item.label .. ' - $' .. dynamicPrice,
        type = 'success'
    })
end)

Robbery Mechanics for Criminal Roleplay

Vending machines present a natural robbery target for criminal characters. Implement a lockpick or pry-bar mechanic that lets criminals break into machines to steal the cash inside. The robbery should take time, require a minigame like a lockpicking puzzle, generate noise that alerts nearby players and potentially triggers a police dispatch, and yield a variable reward based on how many purchases the machine has processed since its last restocking or robbery. Track the cash accumulation per machine based on actual player purchases so the robbery reward is realistic and tied to genuine economic activity. After a successful robbery, the machine enters a damaged state where it cannot process purchases until repaired by a mechanic or automatically after a cooldown period. This creates consequences for criminal activity that affect the broader community and generates repair work for mechanics, further enriching the economic ecosystem.

Player-Owned Shops and Custom Storefronts

Extend the vending machine framework into a full point-of-sale system for player-owned businesses. Business owners can place custom shop counters at their properties, configure what items they sell, set their own prices, and manage their inventory through a management NUI. The shop system uses the same underlying purchase and stock mechanics as vending machines but adds an owner layer with revenue tracking, expense management, employee permissions, and profit reports. Employees clocked into the business can access the register to process sales manually for roleplay interactions, while the automated system handles sales when no employee is present. Revenue from automated sales goes to the business bank account minus a configurable tax percentage that flows to the server's government treasury. This creates a complete retail economy where players manufacture or source goods, stock their shops, price competitively against other businesses, and earn passive income from their investment.

Performance Optimization and Target Integration

Scanning for prop models every frame would destroy server performance. Instead, use a tiered detection approach. Run a broad scan every 2 seconds in a 15-meter radius to find nearby vending machine props. Cache discovered machine locations and only do precise distance checks against the cache in subsequent frames. When the player moves more than 20 meters from any cached machine, clear the cache and trigger a fresh scan. For servers using target systems like ox_target or qb-target, register target zones on discovered props rather than using proximity prompts. Target zones are more performant because they only activate when the player aims at the object, eliminating the need for continuous distance checking entirely. The target approach also provides a cleaner UI experience with context-sensitive interaction options that appear directly on the machine model rather than as floating text prompts.

-- client/target.lua (ox_target integration)
local registeredMachines = {}

CreateThread(function()
    for machineType, config in pairs(Config.Machines) do
        for _, model in ipairs(config.models) do
            exports.ox_target:addModel(model, {
                {
                    name     = 'vending_' .. machineType,
                    icon     = 'fas fa-shopping-cart',
                    label    = 'Use ' .. config.label,
                    onSelect = function(data)
                        OpenMachineMenu(machineType, data.entity)
                    end,
                    distance = Config.InteractDistance,
                },
            })
        end
    end
end)

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.