Description
A bank/ATM script for ESX servers. The player walks up to any ATM prop, presses E, and a small NUI menu shows their cash and bank balances with deposit and withdraw fields. This is the most-requested money primitive on any roleplay server and the foundation every other economy feature builds on.
Prompt Template
You are writing a FiveM resource for es_extended (stable, exports getSharedObject).
Produce ONE file containing a client section and a server section, banner-separated.
Framework: ESX. Get the object with:
local ESX = exports['es_extended']:getSharedObject()
Client:
- A CreateThread loop that uses GetClosestObjectOfType against ATM prop hashes
[`prop_atm_01`, `prop_atm_02`, `prop_atm_03`, `prop_fleeca_atm`] within 2.0m of
GetEntityCoords(PlayerPedId()). When near, show a help notification and on
IsControlJustReleased(0, 38) TriggerServerEvent("bank:requestBalance").
- Sleep the loop (Wait(1500)) when no ATM is near; Wait(0) only when near.
- A "bank:openMenu" net event that SendNUIMessage opens the UI and SetNuiFocus(true,true).
- RegisterNUICallback handlers "deposit", "withdraw", "close" that tonumber the
amount and TriggerServerEvent. "close" calls SetNuiFocus(false,false).
Server:
- "bank:requestBalance": get xPlayer via ESX.GetPlayerFromId(source), send bank
account money and getMoney() back to the client.
- "bank:deposit": math.floor the amount; REJECT if amount <= 0 or amount >
xPlayer.getMoney(); else removeMoney + addAccountMoney("bank").
- "bank:withdraw": same clamps against the bank balance; removeAccountMoney +
addMoney.
Hard requirements:
- ALL money validation is server-side. Never trust the client amount.
- Use PlayerPedId(), not GetPlayerPed(-1).
- Register net events with RegisterNetEvent so the server actually receives them.
Expected Output
The reference Lua at content/expected-outputs/economy/01-bank-atm-deposit-withdraw.lua implements ATM proximity detection, a NUI open/deposit/withdraw/close flow on the client, and a server side that clamps every amount against the real cash/bank balances. In fxmanifest.lua the proximity + NUI callbacks are client_script 'client.lua' and the money mutations are server_script 'server.lua'.
-- Resource: bank-atm (ESX)
-- ATM/bank deposit + withdraw + balance check.
-- Client opens the ATM near a prop; server is authoritative over money.
-- ===== client.lua =====
local ESX = exports['es_extended']:getSharedObject()
local atmModels = { "prop_atm_01", "prop_atm_02", "prop_atm_03", "prop_fleeca_atm" }
local nearAtm = false
CreateThread(function()
while true do
local sleep = 1500
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
local obj = nil
for _, model in ipairs(atmModels) do
obj = GetClosestObjectOfType(coords.x, coords.y, coords.z, 2.0, model, false, false, false)
if obj ~= 0 then break end
end
if obj ~= 0 then
nearAtm = true
sleep = 0
ESX.ShowHelpNotification("Press ~INPUT_CONTEXT~ to use the ATM")
if IsControlJustReleased(0, 38) then
TriggerServerEvent("bank:requestBalance")
end
else
nearAtm = false
end
Wait(sleep)
end
end)
RegisterNetEvent("bank:openMenu", function(bankBalance, cashBalance)
SendNUIMessage({ action = "open", bank = bankBalance, cash = cashBalance })
SetNuiFocus(true, true)
end)
RegisterNUICallback("deposit", function(data, cb)
local amount = tonumber(data.amount) or 0
TriggerServerEvent("bank:deposit", amount)
cb("ok")
end)
RegisterNUICallback("withdraw", function(data, cb)
local amount = tonumber(data.amount) or 0
TriggerServerEvent("bank:withdraw", amount)
cb("ok")
end)
RegisterNUICallback("close", function(_, cb)
SetNuiFocus(false, false)
cb("ok")
end)
-- ===== server.lua =====
RegisterNetEvent("bank:requestBalance", function()
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return end
TriggerClientEvent("bank:openMenu", source, xPlayer.getAccount("bank").money, xPlayer.getMoney())
end)
RegisterNetEvent("bank:deposit", function(amount)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return end
amount = math.floor(tonumber(amount) or 0)
if amount <= 0 or amount > xPlayer.getMoney() then return end
xPlayer.removeMoney(amount)
xPlayer.addAccountMoney("bank", amount)
end)
RegisterNetEvent("bank:withdraw", function(amount)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return end
amount = math.floor(tonumber(amount) or 0)
local bank = xPlayer.getAccount("bank").money
if amount <= 0 or amount > bank then return end
xPlayer.removeAccountMoney("bank", amount)
xPlayer.addMoney(amount)
end)
Known Failure Modes
- Client-side money math — Claude sometimes calls addMoney on the client; it has no authority there. Require every balance change in the server section only.
- No amount clamp — without
amount <= 0andamount > balanceguards a cheater deposits/withdraws arbitrary sums. Demand explicit server-side bounds. - Stuck cursor — omitting
SetNuiFocus(false,false)on close traps the mouse. Require a "close" NUI callback that releases focus. - Heavy proximity loop — scanning every frame tanks FPS. Require the loop to
Wait(1500)unless an ATM is within range.
Integration Notes
Split into client.lua and server.lua at the banner comments. In fxmanifest.lua declare client_script 'client.lua', server_script 'server.lua', and a ui_page/files block if you ship the NUI. Depends on es_extended. Test on a dev server: walk to a Fleeca ATM, press E, deposit cash, then /setjob-independent — confirm bank rises and money falls and persists across a reconnect.
Profit Potential
$150–$1800/mo on Tebex (expected ~$500). [INFERRED] priced inside the $50-389 standalone-script band against the signal-scraper corpus (tebex_snapshot n=100, median seller $11.85K/mo) for a rising economy niche.
Trend Signal
↗ rising — [INFERRED] banking/economy is core RP-server infrastructure, steady demand.
Sales Angle
Position as the drop-in banking primitive every ESX server needs — secure, server-validated deposit/withdraw with a tidy NUI, no heavyweight banking framework required. Recommended Tebex price: $59.
Difficulty & Ship Time
beginner · ships in 2-4h.