Money and Accounts
How money works in ESX Legacy, covering cash, bank and black_money accounts, xPlayer account methods, starting money, paychecks, society money and custom accounts.
ESX Legacy stores a player's money in accounts: money (cash), bank and black_money by default. On the server you read and change them through the xPlayer object, for example xPlayer.addAccountMoney('bank', 500, 'Job reward'), and you configure which accounts exist and how much new players start with in es_extended/shared/config/main.lua.
Overview
| Account | Meaning | Notes |
|---|---|---|
money | Cash on hand | Shortcut methods getMoney, addMoney, removeMoney, setMoney |
bank | Bank balance | transferable = false, receives paychecks |
black_money | Dirty money | Used by drug and money-wash scripts |
Accounts are saved as JSON in the users.accounts column, for example {"bank":50000,"money":0,"black_money":0}. ESX saves players every 10 minutes and when they disconnect. Only the server can change money; clients receive updates through the esx:setAccountMoney event.
Configure accounts
Config.Accounts = {
bank = {
label = TranslateCap("account_bank"),
round = true,
transferable = false,
},
black_money = {
label = TranslateCap("account_black_money"),
round = true,
},
money = {
label = TranslateCap("account_money"),
round = true,
},
}
Config.StartingAccountMoney = { bank = 50000 }| Key | Type | Description |
|---|---|---|
label | string | Display name. The defaults are translated through the locale files. |
round | boolean | Round amounts to whole numbers. Defaults to true when omitted. |
transferable | boolean | false stops players from giving or dropping this account through the default inventory. Defaults to transferable. |
Change the starting money
Config.StartingAccountMoney is used when a character is created, and as the fallback value for any account a loading player does not have yet. New players get no cash by default. To give them some:
Config.StartingAccountMoney = { money = 500, bank = 5000 }Existing players are not affected unless the account is new to them.
Read and change money (server)
All account methods take the account name as a string. Amounts must be numbers.
xPlayer.getAccount(accountName) -- { name, money, label, round, index } or nil
xPlayer.getAccounts(minimal) -- list, or { name = money } when minimal is true
xPlayer.addAccountMoney(accountName, money, reason) -- true on success
xPlayer.removeAccountMoney(accountName, money, reason)
xPlayer.setAccountMoney(accountName, money, reason)
xPlayer.getMoney() / addMoney(money, reason) / removeMoney(money, reason) / setMoney(money)| Name | Type | Description |
|---|---|---|
accountName | string | money, bank, black_money or a custom account. Case-insensitive lookup. |
money | number | Amount. Must be greater than 0 for add/remove, at least 0 for set. |
reason | string? | Free text passed to the money events, useful for logging. |
Return values and errors
removeAccountMoneyreturnsfalsewhen the balance would go negative or when rounding turns the amount into0. It never allows negative balances.addAccountMoneyreturnsfalseif the new balance would overflow.- An unknown account name, a non-number amount, or an amount of
0or less raises a Lua error with the default inventory. Validate input first, especially values that came from a client.
Example: a safe purchase
ESX = exports['es_extended']:getSharedObject()
local PRICE <const> = 250
RegisterNetEvent('coffee:buy', function()
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return end
-- Price is decided on the server, never sent by the client
if not xPlayer.removeAccountMoney('money', PRICE, 'Coffee shop') then
return xPlayer.showNotification('Not enough cash')
end
xPlayer.addInventoryItem('coffee', 1)
end)Removing the money first and checking the boolean is enough to prevent the classic "buy with no money" exploit. Do not trust a price, account or amount sent by the client; see Callbacks for validating client input.
Example: pay from bank, fall back to cash
local function charge(xPlayer, amount, reason)
if xPlayer.removeAccountMoney('bank', amount, reason) then
return 'bank'
end
if xPlayer.removeAccountMoney('money', amount, reason) then
return 'money'
end
return nil
endRead money on the client
The client gets a copy of the accounts in ESX.PlayerData.accounts. Use it for display only.
local bank = ESX.GetAccount('bank')
if bank then
print(('Bank balance: $%s'):format(ESX.Math.GroupDigits(bank.money)))
end
RegisterNetEvent('esx:setAccountMoney', function(account)
print(('%s is now %s'):format(account.name, account.money))
end)Money events
| Event | Side | Arguments |
|---|---|---|
esx:addAccountMoney | Server | source, accountName, money, reason |
esx:removeAccountMoney | Server | source, accountName, money, reason |
esx:setAccountMoney | Server | source, accountName, money, reason |
esx:setAccountMoney | Client | account (the full account table) |
A simple server-side money log:
AddEventHandler('esx:removeAccountMoney', function(playerId, accountName, amount, reason)
if amount >= 100000 then
print(('[money] %s removed %s from %s (%s)'):format(playerId, amount, accountName, reason))
end
end)Add a custom account
Add the account to Config.Accounts
Config.Accounts = {
bank = { label = TranslateCap("account_bank"), round = true, transferable = false },
black_money = { label = TranslateCap("account_black_money"), round = true },
money = { label = TranslateCap("account_money"), round = true },
crypto = { label = "Crypto", round = false, transferable = false },
}Use a plain string label unless you also add a locale key.
Optionally give a starting balance
Config.StartingAccountMoney = { bank = 50000, crypto = 0 }Restart the server
When players load, ESX builds their account list from Config.Accounts. Existing players get the new account with the starting value (or 0), and it is saved into users.accounts on the next save. Now xPlayer.addAccountMoney('crypto', 0.25, 'Mining') works, and /giveaccountmoney accepts crypto.
ox_inventory
With ox_inventory, accounts listed in the inventory:accounts convar (default ["money", "black_money"]) are stored as items in the ox inventory, not in users.accounts. ESX routes the account methods to ox_inventory and returns false instead of raising an error on invalid input. Only add a custom account to that convar if it should be a physical item. See ox_inventory.
Paychecks
Salaries come from the player's job grade (job_grades.salary) and are paid into bank.
| Config key | Default | Description |
|---|---|---|
Config.EnablePaycheck | true | Turn paychecks on or off |
Config.PaycheckInterval | 7 * 60000 | Interval in milliseconds |
Config.OffDutyPaycheckMultiplier | 0.5 | Off-duty players get this share of their salary |
Config.EnableSocietyPayouts | false | Pay salaries from the job's society account (needs esx_society) |
Config.LogPaycheck | false | Log paychecks to Discord |
Config.PaycheckBatchSize, PaycheckBatchDelay, PaycheckJitter | 100, 250, 30000 | Spread payouts on big servers |
With society payouts enabled, a player whose company cannot cover the salary gets a "company has no money" notification and is not paid. Jobs without a registered society are paid normally. xPlayer.togglePaycheck(false) pauses paychecks for one player.
Society (company) money
Company money lives in esx_addonaccount shared accounts named society_<job>, not on the player. Use the GetSharedAccount export on the server:
local account = exports.esx_addonaccount:GetSharedAccount('society_police')
if account then
account.addMoney(5000)
print(('Police funds: %s'):format(account.money))
endThe account object has addMoney, removeMoney and setMoney. Setup is described in Create a job and on the esx_addonaccount page.
Common issues
The account name is not in Config.Accounts. Check spelling; cash is money, not cash.
The amount was 0, negative, nil or a string. Convert with tonumber and check > 0 before calling.
The player was online. ESX holds accounts in memory and saves them back. Change online players through xPlayer methods or the admin commands.
round = true rounds every amount. Set round = false for accounts that need decimals.
Related
Frequently asked questions
Independent documentation
Last updated on
Admin Permissions
How to become admin in ESX Legacy, using users.group, /setgroup, ACE permissions in server.cfg, Config.CommandPermissions and custom admin-only commands.
Callbacks
How to use server and client callbacks in ESX Legacy, with callback and await (promise) styles, timeouts, rate limits and server-side validation.