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
- Define ATM positions and per-ATM cooldowns in Config.
- Use qb-target on the client to make ATMs interactable.
- When a player starts a robbery, client sends the target ATM id and current coords to server.
- Server validates: police on duty, ATM not on cooldown, player is close to known ATM position (distance check), and player inventory (if required).
- 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
| Step | What to verify |
|---|---|
| Install | Place folder in resources, add ensure qb-atm-robbery to server.cfg |
| Dependencies | qb-core and qb-target present and started |
| Interaction | Approach ATM and see qb-target option |
| Server validation | Trigger with insufficient police and confirm rejection |
| Cooldown | Rob ATM, then try again and confirm cooldown prevents repeat |
| Reward | Server 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
- FiveM docs — native references and server scripting guidance.
- QBCore GitHub — project and community examples.
- qb-target repo — setup and API.