EndCore characters have two kinds of money: cash they carry and bank money that stays safe. With en-inventory running, cash is a real item. It takes up space, can be looted from a death bag and can be dropped. Bank money is always a number in the database.
Money types
The keys of defaultMoney in config/shared.lua define which money types exist and what new characters start with.
Key in config/shared.lua | Default | What it does |
|---|---|---|
defaultMoney | { cash = 500, bank = 1000 } | Starting balances. These keys are the valid money types. |
moneyTypes | { cash = 'Cash', bank = 'Bank' } | Display labels |
Money rules in config/server.lua:
| Key | Default | What it does |
|---|---|---|
money.dontAllowMinus | { 'cash', 'bank' } | RemoveMoney refuses to take these below 0, and SetMoney clamps negatives to 0 |
money.roundingMode | 'floor' | 'floor' or 'round'. Money is always stored in whole units. |
money.cashItem | 'cash' | The item that cash becomes while en-inventory runs. nil or false keeps cash as a database balance. |
-- config/server.lua
money = {
dontAllowMinus = { 'cash', 'bank' },
roundingMode = 'floor',
cashItem = 'cash',
},Changing money
Every change goes through the same rules:
- Amounts are rounded to whole numbers using
roundingMode. A value that is not a number becomes 0. AddMoneyandRemoveMoneyneed an amount greater than 0 after rounding, and a valid money type.- Every change is printed to the console with
[MONEY]and posted to theplayermoneywebhook. - Every change fires
encore:server:onMoneyChangeandencore:client:onMoneyChange, which carry the new balance.
| Player function | Export by source | Returns |
|---|---|---|
Functions.AddMoney(moneyType, amount, reason?) | AddMoney(source, moneyType, amount, reason?) | boolean |
Functions.RemoveMoney(moneyType, amount, reason?) | RemoveMoney(source, moneyType, amount, reason?) | boolean |
Functions.SetMoney(moneyType, amount, reason?) | none | boolean |
Functions.GetMoney(moneyType) | GetMoney(source, moneyType) | number |
Admins can use /money add|remove|set <player> <type> <amount>. The reason is recorded as "Admin command by <name>". See Commands.
Cash as an item
Cash becomes an item when all three are true:
money.cashItemis a non-empty string.- en-inventory is running.
- The item (by default
cash) is defined in en-inventory.
If the item is missing, the console warns once: Item "cash" is not defined in en-inventory; cash stays a database balance.
While cash is an item:
- Reading.
GetMoney('cash')counts the item in the player's inventory and worn backpack. - Adding.
AddMoney('cash', ...)gives the item. If it does not fit, en-core callsexports['en-inventory']:AddItemOrDrop, so the cash lands in a bag at the player's feet and they are told. It returnsfalseonly if even that fails. - Removing.
RemoveMoney('cash', ...)returnsfalseif the player is not carrying enough. Items can never go negative. - Setting.
SetMoney('cash', ...)adds or removes the difference. - Bank is unaffected and always a database balance.
PlayerData.money.cash is still kept up to date as a mirror of the item count. That way the compatibility bridges, /charinfo and ESX account reads show a sensible number.
A player who drops or trades cash items changes their cash balance, because the item is the cash. Scripts should always read cash with GetMoney, never by caching PlayerData.money.cash themselves.
Keeping cash in sync
en-core reconciles the item count with the stored balance, using two metadata keys: cashSynced and cashPending.
- Migration. When a character from before cash was an item logs in, or cash changed while en-inventory was stopped or while the character was offline, the difference is handed out as items once, on login. Starting cash is included.
- Cash that does not fit. Anything that does not fit is stored in
cashPendingand added when there is room. The player sees "No room for $X of your cash. Make space and it will be added." - Inventory changes. When en-inventory reports that a player's inventory changed, en-core refreshes that player's cash after a 250 ms debounce.
- Inventory restarts. When en-inventory starts or restarts, every online player is refreshed.
Mirror-only updates are sent as encore:client:onMoneySync(moneyType, balance) and encore:server:onMoneySync(source, moneyType, balance). They do not fire onMoneyChange, so no "money received" toast appears when a player simply picks up or drops cash.
Client side
When encore:client:onMoneyChange arrives, the client stores the balance it carries (it does not re-apply the amount), then fires the local event encore:client:moneyChanged(moneyType, balance, action, reason). Sync updates fire the same local event with the action 'sync'. A HUD only needs to listen to that one event.
Events
| Side | Event | Payload |
|---|---|---|
| Server | encore:server:onMoneyChange | source, moneyType, amount, action, reason, balance |
| Server | encore:server:onMoneySync | source, moneyType, balance |
| Client (net) | encore:client:onMoneyChange | moneyType, amount, action, reason, balance |
| Client (net) | encore:client:onMoneySync | moneyType, balance |
| Client (local) | encore:client:moneyChanged | moneyType, balance, action, reason |
action is 'add', 'remove' or 'set', or 'sync' on the local client event.
Examples
Sell an item for cash:
local player = exports['en-core']:GetPlayer(source)
if not player then return end
if player.Functions.RemoveItem('scrap_metal', 5) then
player.Functions.AddMoney('cash', 120, 'Sold 5 scrap metal')
endCharge for a service, preferring cash then bank:
local price = 300
if exports['en-core']:GetMoney(source, 'cash') >= price then
exports['en-core']:RemoveMoney(source, 'cash', price, 'Vehicle repair')
elseif not exports['en-core']:RemoveMoney(source, 'bank', price, 'Vehicle repair') then
encore.notify(source, { description = 'You cannot afford this.', type = 'error' })
endLog large transfers:
AddEventHandler('encore:server:onMoneyChange', function(source, moneyType, amount, action, reason, balance)
if amount >= 10000 then
exports['en-core']:Log('playermoney', ('%s %s %d %s (%s), now %d'):format(
GetPlayerName(source), action, amount, moneyType, reason or 'no reason', balance))
end
end)Update a HUD on the client:
AddEventHandler('encore:client:moneyChanged', function(moneyType, balance, action, reason)
if moneyType == 'cash' then
-- update your cash display with balance
end
end)