Banking that survives
a logout.
rde_banking is a full account system for ox_core — deposits, transfers, investments, loans, prestige tiers — with one thing most FiveM banking scripts get wrong: payouts that actually reach the recipient, whether they're online or not.
Overview
rde_banking is a complete account system built directly against ox_core — no ESX or QBCore compatibility bridge, no framework-agnostic abstraction layer standing between your code and the actual data. It shipped originally as an NPWD phone app; the NPWD/Module Federation frontend is gone, the money logic underneath it never changed.
What it replaces
The old NPWD-based build required a React/Webpack toolchain just to change a button. The current version is plain HTML/CSS/JS running on rde_phone's SDK — no build step, and the phone app can be edited and reloaded like any other resource file.
Quick Start
-- rde_phone must start first — the phone app registers into it
ensure ox_core
ensure ox_lib
ensure oxmysql
ensure ox_inventory
ensure rde_phone
ensure rde_banking
The phone app registers itself automatically on start via the same RegisterPhoneApp export any third-party app uses — see the rde_phone App Registry docs for the mechanism itself.
Architecture
The phone app doesn't talk to rde_banking's server through rde_phone's SDK bridge — that bridge only reaches rde_phone's own server callbacks. rde_banking's phone frontend talks directly to its own resource's client NUI callback, which relays to its own server callbacks via lib.callback. rde_phone's SDK is used only for theme, icons, and shell integration (notify/close).
RegisterNUICallback('phone:request', function(data, cb)
local ok, result = pcall(function()
return lib.callback.await('rde_banking:cb:phone' .. data.action, false, data.payload)
end)
cb(ok and result or { error = true })
end)
Tiers & Fees
Tier is calculated from lifetime transaction volume, not current balance — spending and earning both count. Higher tiers get lower transfer fees and higher daily deposit/withdraw limits.
Bronze
Starting tier, baseline fees and limits.
Silver
Reduced transfer fee, higher daily caps.
Gold
Meaningful fee reduction, high daily caps.
Platinum
Near-minimum fees, near-max limits.
Black
Lowest fee tier, highest limits — reserved for high-volume accounts.
Investments
Three risk tiers, each with a min/max investment amount, a return-rate range, and a fixed duration in hours. Investments mature automatically — no action needed to collect, the payout (or loss) posts and a phone notification fires the moment it's ready, even if the app isn't open.
| Risk | What happens |
|---|---|
| Low | Smaller return range, shorter odds of loss. |
| Medium | Balanced return/duration profile. |
| High | Widest return range — biggest upside, biggest downside. |
exports.rde_phone:PushNotification() exists for — see rde_phone's docs.
Loans
Four loan amounts, each gated by a minimum credit score. Credit score starts at a baseline and improves when a loan is fully paid off. Interest accrues once, at approval — pay it down in any increment, not all at once.
Exports
Two of these exist specifically so other resources can move money without requiring the target player to be online — built for the rde_bodyguards marketplace, usable by anything.
| Export | Signature | Notes |
|---|---|---|
| AdjustBalanceByCharId | exports.rde_banking:AdjustBalanceByCharId(charId, amount, description, txType) | Works offline. Writes directly to the account row — no live player object required. Refuses to create an account that doesn't exist. |
| GetBalanceByCharId | exports.rde_banking:GetBalanceByCharId(charId) | Offline-safe read, returns 0 if no account exists. |
| AddMoney / RemoveMoney | exports.rde_banking:AddMoney(src, amount, description) | Online-only — requires a live source. |
| GetPlayerBalance | exports.rde_banking:GetPlayerBalance(src) | Online-only. |
Common Pitfalls
lib.callback.await() has no server-to-server variant
It's tempting to call it from inside another server callback to reuse a list-fetch (transactions/investments/loans). It silently times out — the awaiting mechanism only bridges client → server, never server → server.
Fix: extract the shared query logic into a plain Lua function (GetTransactionsForPlayer, etc.) that both callbacks call directly.
Online-only payouts silently fail for offline recipients
The original direct-trade system paid a seller via AddMoney(source, ...) — if the seller wasn't online, the money simply never arrived, with no error surfaced anywhere.
Fix: AdjustBalanceByCharId writes to the DB row directly, no live source required. Use it for any payout where the recipient might not be online.
FAQ
Does this need ESX or QBCore?
No. It's written directly against ox_core — no compatibility bridge, no framework-agnostic abstraction layer. If your server is still on ESX, this is a reason to finish the migration, not a blocker.
Can another resource take money from a player without them being online?
Yes — that's exactly what AdjustBalanceByCharId is for. It requires the target to already have an account row (won't invent one for a charId that's never banked).
Do I need the ATM/teller world UI if I only want the phone app?
They share the same money logic but are independent entry points — you can disable one without breaking the other.
Changelog
Offline-safe balance exports added, powering the rde_bodyguards public marketplace.
NPWD/Module Federation removed entirely. Phone app rebuilt on rde_phone's SDK.
Deposit/withdraw/transfer/investment/loan logic extracted into shared functions — no duplication between the world-UI and the phone app.