Banking System Architecture
A banking system is the financial backbone of any FiveM roleplay server, handling everything from simple cash deposits to complex inter-player transfers and shared organization accounts. Most frameworks like QBCore and ESX include basic money management, but a dedicated banking system extends this with proper account management, transaction logging, and a polished ATM interface that immerses players in the financial side of roleplay. The architecture splits into three layers: the database stores account balances and transaction records, the server validates every financial operation and enforces business rules, and the client provides the ATM and bank counter interfaces through NUI. Every money operation must flow through the server side because client-side money manipulation is the number one exploit vector on FiveM servers. Even displaying a balance should come from a server callback, never from cached client-side data that could be tampered with.
Database Schema for Banking
Your banking database needs to support personal accounts, shared accounts for organizations and businesses, and a comprehensive transaction log. The transaction log is not optional because it serves double duty as both a player-facing feature and an admin tool for investigating money exploits. Design your schema to handle high-throughput operations because busy servers can process hundreds of transactions per minute during peak hours:
CREATE TABLE IF NOT EXISTS bank_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
account_number VARCHAR(20) UNIQUE NOT NULL,
owner_citizenid VARCHAR(50) NOT NULL,
account_type ENUM('personal', 'business', 'gang', 'shared') DEFAULT 'personal',
balance BIGINT DEFAULT 0,
account_name VARCHAR(100) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_frozen BOOLEAN DEFAULT FALSE,
INDEX idx_owner (owner_citizenid),
INDEX idx_type (account_type)
);
CREATE TABLE IF NOT EXISTS bank_transactions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
account_id INT NOT NULL,
type ENUM('deposit', 'withdraw', 'transfer_in', 'transfer_out', 'paycheck', 'purchase') NOT NULL,
amount BIGINT NOT NULL,
balance_after BIGINT NOT NULL,
description VARCHAR(255) DEFAULT NULL,
other_account VARCHAR(20) DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_account (account_id),
INDEX idx_created (created_at),
FOREIGN KEY (account_id) REFERENCES bank_accounts(id)
);
CREATE TABLE IF NOT EXISTS bank_account_access (
account_id INT NOT NULL,
citizenid VARCHAR(50) NOT NULL,
permission ENUM('view', 'withdraw', 'full') DEFAULT 'view',
PRIMARY KEY (account_id, citizenid),
FOREIGN KEY (account_id) REFERENCES bank_accounts(id)
);
The bank_account_access table enables shared accounts where multiple players can have different permission levels. A gang leader might have full access to the gang treasury while regular members can only view the balance. Using BIGINT for balance fields prevents overflow issues on servers with inflated economies where player balances can reach into the billions.
Server-Side Transaction Logic
Every financial transaction must be atomic and validated on the server. Use database transactions to ensure that money is never created or destroyed during transfers. When player A sends money to player B, both the deduction from A and the addition to B must succeed together, or neither should apply. Implement validation checks for sufficient balance, frozen account status, daily transfer limits, and minimum transaction amounts. Here is a secure transfer implementation:
RegisterNetEvent('banking:server:transfer', function(targetAccount, amount, description)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
amount = math.floor(tonumber(amount) or 0)
if amount <= 0 then
TriggerClientEvent('QBCore:Notify', src, 'Invalid amount', 'error')
return
end
local citizenid = Player.PlayerData.citizenid
local senderAccount = GetPlayerPrimaryAccount(citizenid)
if not senderAccount or senderAccount.is_frozen then
TriggerClientEvent('QBCore:Notify', src, 'Account unavailable', 'error')
return
end
if senderAccount.balance < amount then
TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
return
end
-- Check daily transfer limit
local todayTransfers = GetDailyTransferTotal(senderAccount.id)
if todayTransfers + amount > Config.DailyTransferLimit then
TriggerClientEvent('QBCore:Notify', src, 'Daily limit exceeded', 'error')
return
end
-- Atomic transfer using database transaction
local success = MySQL.transaction.await({
{
query = 'UPDATE bank_accounts SET balance = balance - ? WHERE id = ? AND balance >= ?',
values = {amount, senderAccount.id, amount}
},
{
query = 'UPDATE bank_accounts SET balance = balance + ? WHERE account_number = ?',
values = {amount, targetAccount}
},
{
query = 'INSERT INTO bank_transactions (account_id, type, amount, balance_after, description, other_account) VALUES (?, "transfer_out", ?, (SELECT balance FROM bank_accounts WHERE id = ?), ?, ?)',
values = {senderAccount.id, amount, senderAccount.id, description or 'Transfer', targetAccount}
},
})
if success then
TriggerClientEvent('QBCore:Notify', src, 'Transfer complete: $' .. amount, 'success')
TriggerClientEvent('banking:client:refreshBalance', src)
else
TriggerClientEvent('QBCore:Notify', src, 'Transfer failed', 'error')
end
end)
Notice the WHERE balance >= ? clause in the deduction query, which acts as a final guard against race conditions where two simultaneous transfers could overdraw the account. This database-level check is essential even though you already verify the balance in Lua, because multiple requests can arrive between your check and the actual update.
ATM User Interface
The ATM interface is a compact NUI panel that provides quick access to core banking functions: check balance, deposit cash, withdraw cash, and transfer money. Keep the design clean and familiar because players instinctively expect an ATM to work like the real-world equivalent. Display the current balance prominently at the top, with action buttons below for each operation. The deposit and withdrawal views should include preset amount buttons for common values like $100, $500, $1000, and $5000 alongside a custom amount input field. For transfers, provide fields for the recipient account number and amount, along with an optional memo field. Show a confirmation step before executing any transaction to prevent accidental clicks from costing players money. Include a recent transactions list that displays the last 10 entries so players can verify their financial activity without needing to visit a full bank branch. The ATM UI should feel snappy, so fetch the balance and transaction data in a single callback when the menu opens rather than making separate requests for each piece of information.
ATM Interaction Setup
Place ATM interaction points at the existing ATM prop locations throughout the GTA map. FiveM provides a list of ATM model hashes that you can iterate over to find all ATM props in the game world. Use a target system like ox_target for clean interaction, or fall back to proximity checks near each ATM prop. When the player interacts with an ATM, play an animation of the player using the ATM, then open the NUI panel:
local atmModels = {
'prop_atm_01', 'prop_atm_02', 'prop_atm_03',
'prop_fleeca_atm', 'v_5_b_atm1'
}
-- Using ox_target for ATM interaction
for _, model in ipairs(atmModels) do
exports.ox_target:addModel(GetHashKey(model), {
{
name = 'use_atm',
icon = 'fas fa-credit-card',
label = 'Use ATM',
onSelect = function(data)
local ped = PlayerPedId()
local atmCoords = GetEntityCoords(data.entity)
-- Face the ATM
TaskTurnPedToFaceCoord(ped, atmCoords.x, atmCoords.y, atmCoords.z, 1000)
Wait(1000)
-- Play ATM animation
RequestAnimDict('mini@atmenter')
while not HasAnimDictLoaded('mini@atmenter') do Wait(10) end
TaskPlayAnim(ped, 'mini@atmenter', 'enter', 8.0, -8.0, -1, 0, 0, false, false, false)
-- Open ATM UI
QBCore.Functions.TriggerCallback('banking:server:getAccountData', function(data)
SetNuiFocus(true, true)
SendNUIMessage({
action = 'openATM',
balance = data.balance,
transactions = data.recentTransactions,
accountNumber = data.accountNumber
})
end)
end
}
})
end
Transaction History and Statements
Transaction history transforms your banking system from a simple deposit and withdrawal machine into a full financial management tool. Players should be able to view their complete transaction history at bank branch locations, filtered by date range, transaction type, or amount. Each transaction entry should display the date, type, amount, resulting balance, description, and the other party involved for transfers. Implement pagination on the server side because loading thousands of transactions at once will freeze the NUI frame and spike server memory usage. Return 20-30 transactions per page and let the player load more as needed. For bank branch locations, offer additional features beyond what ATMs provide, such as opening new accounts, managing shared account permissions, generating account statements for a specific period, and applying for loans if your server supports that mechanic. Store transaction descriptions as human-readable strings so that automated transactions from jobs, shop purchases, and government taxes all show clear entries that players can understand without context.
Bank Robbery Mechanics
Bank robberies are one of the most exciting events on any roleplay server, creating high-stakes scenarios that involve criminals, police, hostage negotiators, and bystanders. A well-designed robbery system includes multiple phases: casing the bank, initiating the heist, hacking or drilling security systems through minigames, loading the loot, and escaping with the police in pursuit. Start by defining which banks can be robbed, their difficulty tier, cooldown timers, and required items. The robbery should require specific tools like thermite for vault doors, hacking devices for security panels, and duffel bags for carrying the loot. Implement progressive difficulty minigames for each security layer, where failing a hack triggers additional alarms or locks down the vault further:
Config.BankRobberies = {
['fleeca_1'] = {
label = 'Fleeca Bank - Legion Square',
coords = vector3(149.73, -1042.65, 29.37),
vault = vector3(144.87, -1044.16, 29.37),
tier = 1, -- 1=Fleeca, 2=Paleto, 3=Pacific Standard
cooldown = 7200, -- 2 hours
minPolice = 3,
requiredItems = {'electronickit', 'thermite'},
reward = { min = 40000, max = 80000, markedBills = true },
securityLayers = {
{ type = 'hack', difficulty = 'easy', time = 30 },
{ type = 'thermite', time = 10 },
{ type = 'drill', time = 45 },
},
},
['pacific_standard'] = {
label = 'Pacific Standard Bank',
coords = vector3(255.85, 225.60, 101.88),
vault = vector3(262.20, 222.10, 101.68),
tier = 3,
cooldown = 14400, -- 4 hours
minPolice = 6,
requiredItems = {'electronickit', 'thermite', 'advancedlaptop'},
reward = { min = 200000, max = 400000, markedBills = true },
securityLayers = {
{ type = 'hack', difficulty = 'hard', time = 20 },
{ type = 'hack', difficulty = 'hard', time = 20 },
{ type = 'thermite', time = 8 },
{ type = 'drill', time = 60 },
{ type = 'hack', difficulty = 'expert', time = 15 },
},
},
}
Use marked bills as the robbery reward instead of clean cash, forcing criminals to launder the money through additional gameplay loops like money laundering locations or dirty money exchanges. This extends the robbery roleplay beyond the heist itself and creates opportunities for police investigations. Notify the police dispatch system when a robbery begins and track the robbery progress server-side so officers can respond tactically based on how far the criminals have progressed.
Security and Anti-Exploit Measures
Financial systems are the primary target for exploiters because money directly translates to in-game power. Beyond the server-side validation already discussed, implement several additional security layers. Add rate limiting to all banking events so that a single player cannot fire hundreds of deposit or transfer requests per second. Log every financial transaction with the source player's identifiers and timestamp so administrators can trace money flow and identify duplication exploits. Implement a transaction reversal system that admins can use to undo fraudulent transactions when exploits are discovered. Set maximum single-transaction limits and daily cumulative limits that scale with the player's account age and total playtime, making freshly created accounts less useful for money laundering. Monitor for suspicious patterns like rapid round-trip transfers between two accounts, deposits that exactly match the withdrawal amount from another player within seconds, or balance increases without corresponding transaction records. Send Discord webhook alerts when suspicious activity is detected so your moderation team can investigate in real time without waiting for player reports. Consider implementing a frozen account system where admin can lock accounts during investigations, preventing the exploited money from being spent or transferred while the issue is resolved.
Integrating with the Server Economy
Your banking system should serve as the central hub for all monetary flow on the server. Route job paychecks through the banking system so players receive their salary as a bank deposit with a clear transaction record showing which job paid them and how much. Connect shop purchases to trigger bank withdrawals when players pay with their card instead of cash, creating a paper trail that adds realism and gives players a reason to use the banking system beyond simple storage. Implement automatic billing for recurring costs like property taxes, vehicle insurance, and business operating expenses that deduct from the player's bank account at regular intervals. If a player's account has insufficient funds for an automatic payment, the system should log a failed payment and trigger consequences like property seizure warnings or insurance lapses. Link the banking system to your phone resource so players can check their balance, view recent transactions, and make quick transfers without physically visiting an ATM or bank branch. This integration transforms the banking system from an isolated feature into the financial nervous system that connects every economic activity on your server.