FiveM Economy Scripts Troubleshooting FAQ: 12 Common Issues Fixed
Economy scripts are the financial backbone of every roleplay server. When money breaks, jobs stop paying, shops stop selling, and players lose trust in minutes. This guide covers the 12 most critical economy failures on ESX, QBCore, and QBox — duplication exploits, banking errors, shop prices, and inflation management — with diagnostic steps and working code.

Economy scripts form the financial backbone of every roleplay server. When money systems break, the ripple effect touches everything else — jobs stop paying, shops stop selling, and players lose trust within minutes. The economy is also the #1 target for exploits, so fixing it is equal parts bug-hunting and security work.
This guide walks through the twelve economy failures we see most often on ESX, QBCore, and QBox servers. Each section covers how to diagnose the root cause, the fix that resolves it in most cases, and the security trap that catches experienced admins.
Quick Diagnosis Table

Before diving into individual issues, match the symptom you see to the most likely root cause.
| Symptom | Most likely cause | First thing to check |
|---|---|---|
| Players report infinite money | Client-triggered money event | Grep for RegisterNetEvent with addMoney |
| Banking returns nil balance | Missing account record | SELECT * FROM bank_accounts WHERE identifier = ? |
| Balance caps at ~2.1B | INT column on large server | DESCRIBE bank_accounts |
| ATM prompt doesn't appear | Prop hash mismatch | Map's actual ATM props vs config |
| Shop items all priced at 0 | Config Lua syntax error | Server console on resource start |
| Invoice send fails silently | Name-based lookup breaks | Use identifier-based query |
| Dirty money won't convert | Account type not registered | addon_accounts table |
| Shared account denied | Member not in access table | shared_account_members |
| Salary never arrives | Timer not running | Debug-print inside loop |
| Transaction log fills DB | No cleanup | Old logs still present |
| Taxes bypassed | Some income event skips tax | Grep every addMoney call |
| Stock market NaN crash | Unbounded price calc | Add NaN + floor + ceiling guards |
| Total money grows each week | More faucets than sinks | Weekly SUM query |
Use this table as a triage filter. Once you've narrowed the bucket, jump to the detailed section below.
1. Money Duplication Exploits
Money duplication is the most serious economy issue — one exploit can destroy server balance in an hour. Three root causes:
- Client-side events can add money without server validation
- Race conditions in transaction processing allow double-spending
- Disconnect during a transaction duplicates the amount
Audit your money events
-- BAD: Client can trigger money addition
RegisterNetEvent('economy:addMoney')
AddEventHandler('economy:addMoney', function(amount)
-- Exploitable. Any client can trigger this with any amount.
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.addMoney(amount)
end)
-- GOOD: Server validates and sets the amount
RegisterNetEvent('economy:requestPayment')
AddEventHandler('economy:requestPayment', function(jobId)
local source = source -- capture immediately
local xPlayer = ESX.GetPlayerFromId(source)
if not validateJobCompletion(source, jobId) then return end
local amount = Config.Jobs[jobId].pay -- amount comes from SERVER config
xPlayer.addMoney(amount)
end)
Fix checklist, in order:
- Every money-adding event validates on the server side
local source = sourcecaptured immediately at handler start (prevents spoofing via async calls)- Transaction amounts read from server config, never from event payload
- Anti-cheat monitors for impossible balance changes between consecutive writes
- Player disconnects during mid-transaction clean up in-flight state
2. Banking Errors
Banking systems store player balances in the database. Most errors are schema problems:
-- Verify table structure
DESCRIBE bank_accounts;
-- Check player has an account
SELECT * FROM bank_accounts WHERE identifier = 'char1:abc123';
-- Catch negative balances (shouldn't exist)
SELECT * FROM bank_accounts WHERE balance < 0;
If the balance column uses INT, it caps around 2.1 billion. For servers with large economies, switch to BIGINT:
ALTER TABLE bank_accounts MODIFY COLUMN balance BIGINT NOT NULL DEFAULT 0;
Missing account records show as nil-balance errors. Add a safety net: on player login, ensure an account row exists:
-- ESX example
MySQL.query.await(
'INSERT IGNORE INTO bank_accounts (identifier, balance) VALUES (?, 0)',
{ xPlayer.identifier }
)
3. ATM Not Working
ATM interactions use target zones tied to ATM prop models. If the prompt doesn't appear, the prop hash list in your config doesn't match the props on your map.
-- Default GTA V ATM props
local atmProps = {
`prop_atm_01`,
`prop_atm_02`,
`prop_atm_03`,
`prop_fleeca_atm`,
`prop_atm_04`,
}
Custom maps (custom MLOs for banks, corner stores, rural areas) often use non-default ATM props. Inspect the actual prop on your map with GetEntityModel and add it to your config.
If the prompt appears but the ATM doesn't open, check F8 DevTools — the ATM interface is usually a web UI that can fail to load on slow clients.
4. Shop Prices Wrong
Shop systems pull prices from either a config file or a database table. Diagnose in this order:
- All items at 0 — the config file has a Lua syntax error. Check the server console on start for
error loading config.lua. - Wrong specific prices — a runtime override script is modifying them. Grep the item name across all resources.
- Prices missing entirely — the shop config doesn't reference your framework's inventory item list correctly.
-- Example shop config
Config.Shops = {
['247store'] = {
items = {
{ name = 'bread', price = 5, label = 'Bread' },
{ name = 'water', price = 3, label = 'Water' },
},
},
}
For database-backed shops:
SELECT * FROM shop_items WHERE shop_id = '247store';
If the table is empty or corrupt, re-run the shop script's SQL install file.
5. Billing System Failing
Invoice systems require both sender and receiver identifiers. Billing fails most often when the script tries to find a player by character name and the name contains special characters that break the SQL query.
-- Check the billing table
SELECT * FROM bills ORDER BY created_at DESC LIMIT 10;
-- Verify schema
DESCRIBE bills;
The fix: use parameterized queries and identifier-based lookups (citizenid, license) instead of name matching.
-- BAD: name-based, fragile
MySQL.query.await(
("SELECT * FROM players WHERE firstname = '%s' AND lastname = '%s'"):format(first, last)
)
-- GOOD: parameterized, identifier-based
MySQL.query.await(
'SELECT * FROM players WHERE citizenid = ?',
{ targetCitizenId }
)
6. Black Money Not Converting
Black money (dirty money) is a separate account type in most frameworks. Conversion requires a laundering script with a running event handler.
-- ESX account access
xPlayer.getAccount('black_money')
xPlayer.addAccountMoney('black_money', 100)
-- QBCore money types
PlayerData.money.crypto
Player.Functions.AddMoney('crypto', 100)
Verify the black_money account type is registered:
-- ESX addon_accounts
SELECT * FROM addon_accounts WHERE name = 'black_money';
If missing, re-run the ESX install SQL to create default accounts. If present but conversion fails, add a debug-print to the laundering script's server-side handler to confirm the event fires.
7. Shared Account Issues
Shared bank accounts (for businesses, gangs, factions) need multi-user access via a members table.
-- Check shared account exists
SELECT * FROM shared_accounts WHERE account_name = 'gang_ballas';
-- Check members
SELECT * FROM shared_account_members WHERE account_id = 1;
Most shared-account scripts use permission levels per member:
| Level | Can view | Can deposit | Can withdraw | Can add members |
|---|---|---|---|---|
| owner | Yes | Yes | Yes | Yes |
| manager | Yes | Yes | Yes | No |
| member | Yes | Yes | No | No |
If a member is denied, verify their level isn't too low for the action. Some scripts require owner for every action, which is wrong — fix the permission check.
8. Salary Not Depositing
Salary systems run on a timer, typically every 15-30 minutes of in-game duty time:
Citizen.CreateThread(function()
while true do
Wait(Config.PayInterval * 60000) -- minutes to ms
for _, playerId in ipairs(GetPlayers()) do
if isOnDuty(playerId) then
depositSalary(playerId)
end
end
end
end)
Common failures:
Config.PayIntervalis0— infinite tight loop that stalls the serverisOnDutyalways returns false due to job state driftdepositSalarytargets the wrong account type (cash instead of bank)
Drop a debug-print inside the loop to confirm the timer is actually executing:
print(('[salary] tick at %d, %d players'):format(os.time(), #GetPlayers()))
9. Transaction Log Errors
Transaction logs track every money movement for admin review. As logs grow, INSERT operations slow down and eventually fail.
-- Check log table size
SELECT COUNT(*) FROM transaction_logs;
-- Cleanup logs older than 60 days
DELETE FROM transaction_logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 60 DAY);
Schedule the cleanup as a nightly cron job or a txAdmin-scheduled task.
Always wrap log inserts in pcall so a logging failure never blocks a transaction:
pcall(function()
MySQL.insert('INSERT INTO transaction_logs ...', { ... })
end)
10. Tax System Broken
Taxes deduct a percentage from income events. Tax bypass almost always means some income source doesn't route through the tax handler.
local function applyTax(grossAmount)
local taxRate = Config.TaxRate or 0.10
local tax = math.floor(grossAmount * taxRate)
local net = grossAmount - tax
logTax(tax)
return net, tax
end
-- Every income point must go through this
local net, tax = applyTax(jobPaycheck)
xPlayer.addMoney(net)
Audit every addMoney, addAccountMoney, Player.Functions.AddMoney call in your server. If any skip the tax handler, that's your leak.
Also verify Config.TaxRate is set — a missing key silently disables taxes without errors.
11. Crypto/Stock Market Crashes
Market scripts with price fluctuation algorithms can produce invalid values that crash the script:
local function calculateNewPrice(currentPrice, volatility)
local change = math.random(-volatility, volatility) / 100
local newPrice = currentPrice * (1 + change)
-- Bounds checking: NaN, floor, ceiling
if newPrice ~= newPrice then return currentPrice end
if newPrice < 1 then return 1 end
if newPrice > 1000000 then return 1000000 end
return math.floor(newPrice)
end
If the market table is empty on first start, the script may crash trying to read nonexistent price history. Seed initial market data on first run:
CreateThread(function()
local exists = MySQL.scalar.await('SELECT COUNT(*) FROM crypto_markets')
if exists == 0 then
seedDefaultMarkets()
end
end)
12. Inflation Management
Track your server's total money supply weekly:
-- ESX: total money in circulation
SELECT
(SELECT SUM(money) FROM users) +
(SELECT SUM(money) FROM addon_account_data WHERE account_name LIKE 'society_%')
AS total_money;
-- QBCore: bank + cash across all players
SELECT
SUM(JSON_EXTRACT(money, '$.bank')) +
SUM(JSON_EXTRACT(money, '$.cash'))
AS total_money
FROM players;
If total money grows consistently, your server has more faucets than sinks. Add money sinks:
| Sink | Example implementation | Target % of total flow |
|---|---|---|
| Vehicle maintenance | Fuel + oil changes every X km | 10-15% |
| Property tax | Weekly rent on owned houses | 5-10% |
| Insurance premiums | Vehicle + health insurance | 5-10% |
| Repair costs | Vehicle + equipment repairs | 5-10% |
| Consumables | Food, drink, ammunition | 10-15% |
| Transaction fees | ATM, wire transfer fees | 2-5% |
Target a steady-state where faucets match sinks within ±5% per week. Run the SUM query every Monday and log the result for trend tracking.
Recommended Scripts
When you need economy scripts that just work — secure, well-documented, framework-native — check the VertexMods catalog:
- FiveM Economy Scripts — Premium and free economy resources
- Best FiveM Scripts 2026 — Top-rated scripts across all categories
- FiveM Troubleshooting Guide — Broader troubleshooting playbook
- FiveM Error Codes Fixes — Decoding common error messages
Frequently Asked Questions
How do I fix money duplication exploits in my FiveM server?
Money dupes happen when a client-triggered event can add money without server-side validation, or when race conditions let a transaction fire twice. Audit every money-adding event: set amounts server-side only, capture source immediately, validate the action was actually completed before paying out, and never trust any amount passed from the client. Add anti-cheat monitoring for impossible balance jumps.
Why is the banking system throwing errors?
Banking errors are almost always schema problems: the bank_accounts table is missing, the balance column is INT (capping at 2.1B for large servers — use BIGINT), or players don't have account records. Run SELECT COUNT(*) FROM bank_accounts and compare to your player count; missing accounts are the single most common cause of ‘Cannot read balance’ errors.
Why isn't the ATM working in my FiveM server?
ATM failures are target zone issues. The ATM interaction prompt depends on matching prop model hashes to the actual ATM props on your map. Custom maps often use different ATM props than default GTA V, so the banking script's hash list misses them. Add GetHashKey lookups for every ATM prop your map uses, or switch to coordinate-based zones.
Why are shop prices wrong or not loading?
Shop prices come from config files or database tables. Prices showing as 0 usually mean the config file has a Lua syntax error (so the file loaded as empty) — check the server console on start for parse errors. Prices showing as wrong values are almost always drift between the config and a runtime override script. Grep for the item name across all resources to find who's modifying it.
Why is the billing system failing to send invoices?
Billing fails when the target player's identifier can't be resolved, when the billing table is missing, or when the billing event names don't match across resources. The most common cause is a player-name lookup that breaks on special characters — use parameterized queries and identifier-based lookups (citizenid, license) instead of name matching.
Why isn't black money converting properly?
Black money conversion (money laundering) requires the dirty money account type to be registered in your framework. ESX uses addon_accounts — verify black_money is in the addon_accounts table. QBCore uses a money types config; check your shared/main.lua for MoneyTypes. If the account exists but conversion fails, the laundering script's event isn't firing — debug-print the server-side handler.
Why aren't shared bank accounts working?
Shared accounts (for businesses, gangs, factions) need two tables: the account itself plus a members table mapping multiple identifiers to it. If members can't access, their identifier is missing from the members table, or the access permission check is too strict. Most shared-account scripts have per-member permission levels (owner, manager, member) — verify your access test doesn't require owner when the member is just a manager.
