FiveM & QBCore · Practical guide

QBCore ATM Robbery — Secure, Testable Implementation Guide

Design and implement a secure ATM robbery system for QBCore-based FiveM servers. Server-side validation, qb-target integration, cooldowns, police checks, and safe reward handling with.

Stellar AI · Updated 8 September 2026 · 5 min read

This guide walks you through a robust ATM robbery resource for QBCore. It covers design goals, server-authoritative checks, qb-target integration, anti-spoofing mitigations, and complete example files you can drop into a FiveM resource.

Why server authority matters

ATM robberies change player balances and trigger server-wide consequences (police alerts, cooldowns, item rewards). Keep authority server-side: reject all reward and state changes on the client. The client should only request actions and render effects. This reduces cheating and inconsistent state across players.

High-level implementation plan

  1. Define ATM positions and per-ATM cooldowns in Config.
  2. Use qb-target on the client to make ATMs interactable.
  3. When a player starts a robbery, client sends the target ATM id and current coords to server.
  4. Server validates: police on duty, ATM not on cooldown, player is close to known ATM position (distance check), and player inventory (if required).
  5. On success, server marks ATM on cooldown, notifies police, gives reward via QBCore server API, and broadcasts a robbery in progress to nearby clients for particle/sound effects.

Assumptions and dependencies

  • Server uses QBCore export: exports['qb-core']:GetCoreObject().
  • qb-target is available for interaction zones/models.
  • You will add the resource to server.cfg and test on a dev server with a police job configured.

Example files (destination-labelled)

Below are minimal complete files. Place them in a resource folder named qb-atm-robbery/ and add to server.cfg with ensure qb-atm-robbery.

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'

author 'YourName'
description 'QBCore ATM Robbery'
version '1.0.0'

shared_script 'config.lua'

client_scripts {
  'client/main.lua'
}

server_scripts {
  '@oxmysql/lib/MySQL.lua', -- optional if you persist cooldowns
  'server/main.lua'
}

dependencies {
  'qb-core',
  'qb-target'
}

config.lua

Config = {}

-- Example ATM positions, use real coordinates
Config.ATMs = {
  { id = 'atm_1', coords = vector3(89.75, 2.35, 69.63) },
  { id = 'atm_2', coords = vector3(-386.23, -228.25, 37.08) }
}

Config.Cooldown = 600 -- seconds per ATM
Config.RequiredPolice = 2
Config.RewardMin = 200
Config.RewardMax = 1000
Config.MaxDistance = 3.5 -- meters allowed between player and ATM for validation

server/main.lua

local QBCore = exports['qb-core']:GetCoreObject()
local atmCooldowns = {} -- { atmId = timestamp }

local function policeCount()
  local count = 0
  for _, playerId in pairs(QBCore.Functions.GetPlayers()) do
    local Player = QBCore.Functions.GetPlayer(playerId)
    if Player and Player.PlayerData and Player.PlayerData.job and Player.PlayerData.job.name == 'police' then
      count = count + 1
    end
  end
  return count
end

local function findATMById(id)
  for _, atm in ipairs(Config.ATMs) do
    if atm.id == id then return atm end
  end
  return nil
end

RegisterNetEvent('qb-atm-robbery:server:startRobbery', function(atmId, clientCoords)
  local src = source
  local Player = QBCore.Functions.GetPlayer(src)
  if not Player then return end

  -- basic police check
  if policeCount() < Config.RequiredPolice then
    TriggerClientEvent('qb-atm-robbery:client:notify', src, 'Not enough police on duty')
    return
  end

  local atm = findATMById(atmId)
  if not atm then
    TriggerClientEvent('qb-atm-robbery:client:notify', src, 'Invalid ATM')
    return
  end

  -- cooldown check
  local now = os.time()
  if atmCooldowns[atmId] and atmCooldowns[atmId] > now then
    TriggerClientEvent('qb-atm-robbery:client:notify', src, 'ATM is still on cooldown')
    return
  end

  -- distance validation (server trusts client-provided coords minimally)
  local dx = atm.coords.x - clientCoords.x
  local dy = atm.coords.y - clientCoords.y
  local dz = atm.coords.z - clientCoords.z
  local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
  if dist > Config.MaxDistance then
    TriggerClientEvent('qb-atm-robbery:client:notify', src, 'You are too far from the ATM')
    return
  end

  -- mark cooldown
  atmCooldowns[atmId] = now + Config.Cooldown

  -- reward
  local reward = math.random(Config.RewardMin, Config.RewardMax)
  Player.Functions.AddMoney('cash', reward, 'atm-robbery')

  -- alert police and broadcast
  TriggerClientEvent('qb-atm-robbery:client:started', -1, atmId, atm.coords)
  TriggerClientEvent('qb-atm-robbery:client:notify', src, 'Robbery successful, you got $' .. reward)
end)

client/main.lua (overview)

local QBCore = exports['qb-core']:GetCoreObject()

Citizen.CreateThread(function()
  for _, atm in ipairs(Config.ATMs) do
    exports['qb-target']:AddBoxZone(atm.id, atm.coords, 0.8, 0.8, {
      name = atm.id,
      heading = 0,
      debugPoly = false,
      minZ = atm.coords.z - 1.0,
      maxZ = atm.coords.z + 1.0
    }, {
      options = {{
        event = 'qb-atm-robbery:client:attemptRobbery',
        icon = 'fas fa-hand-holding',
        label = 'Rob ATM',
        atmId = atm.id
      }},
      distance = 2.5
    })
  end
end)

RegisterNetEvent('qb-atm-robbery:client:attemptRobbery', function(data)
  local ped = PlayerPedId()
  local coords = GetEntityCoords(ped)
  TriggerServerEvent('qb-atm-robbery:server:startRobbery', data.atmId, { x = coords.x, y = coords.y, z = coords.z })
end)

Security and validation notes

  • Distance checks using client-supplied coords are vulnerable to spoofing. Mitigate by sending the server periodic reliable position heartbeats or validating via server-side position syncing. At minimum, keep checks strict (small MaxDistance).
  • Do server-side police count and cooldown logic (as shown). Never grant money on the client.
  • If you persist cooldowns across restarts, store atmCooldowns in a database (oxmysql) and rehydrate on server start.

Testing checklist

StepWhat to verify
InstallPlace folder in resources, add ensure qb-atm-robbery to server.cfg
Dependenciesqb-core and qb-target present and started
InteractionApproach ATM and see qb-target option
Server validationTrigger with insufficient police and confirm rejection
CooldownRob ATM, then try again and confirm cooldown prevents repeat
RewardServer adds cash; check server logs for authorisation string (atm-robbery)

Next steps and integrations

Want a more polished robbery? Add a minigame on the client (keep outcome server-validated by the server issuing a signed token), animated police dispatch via qb-phone or dispatch resources, and persistent cooldowns with MySQL. If you prototype designs and file layout, Stellar AI can help generate complete resource files and iterate on tests — try the workspace at https://trystellarai.com/app. You can also import your design notes and get a refined implementation plan in the Stellar AI app at https://trystellarai.com/app.

For broader server tips and related tutorials see the Stellar AI blog: https://trystellarai.com/blog.

References & further reading

Build your next system with Stellar AI

Describe one feature, get organized project files, then bring back your errors to keep improving. Start free with no card required. Test generated code in a private development environment before release.

Create your first script free →