A stable economy is the backbone of any roleplay server. Players need to earn money through jobs, spend it in shops, and feel the weight of their financial decisions. QBCore provides solid foundations with its built-in money system, but a great server extends those foundations with custom job paychecks and player-owned businesses.
This guide walks through building a complete economy layer on top of QBCore: automated paychecks for jobs, a configurable shop system, and player-run storefronts.
Understanding QBCore Money
QBCore tracks two types of money for each player:
- Cash — physical money, lost on death, used for casual transactions
- Bank — digital money, requires ATMs or phone banking apps
Both are stored in the player's database and managed through QBCore.Functions.AddMoney, RemoveMoney, and GetMoney.
-- Check a player's balance
local cash = Player.PlayerData.money["cash"]
local bank = Player.PlayerData.money["bank"]
-- Give money
Player.Functions.AddMoney("cash", 500, "job-payment")
-- Remove money
Player.Functions.RemoveMoney("bank", 1000, "shop-purchase")
The third parameter (reason) is logged transactionally, which helps with debugging and audit trails.
Job Paycheck System
Automated paychecks reward players for being on duty. Here's a configurable system that pays based on job grade and on-duty time.
Configuration
-- config.lua
Config = {}
Config.PaycheckInterval = 10 * 60 * 1000 -- 10 minutes in ms
Config.JobPayments = {
["police"] = {
{ grade = 0, amount = 500 },
{ grade = 1, amount = 750 },
{ grade = 2, amount = 1000 },
{ grade = 3, amount = 1250 },
{ grade = 4, amount = 1500 },
},
["ambulance"] = {
{ grade = 0, amount = 400 },
{ grade = 1, amount = 600 },
{ grade = 2, amount = 800 },
{ grade = 3, amount = 1000 },
},
["mechanic"] = {
{ grade = 0, amount = 350 },
{ grade = 1, amount = 500 },
{ grade = 2, amount = 700 },
},
}
Config.Bonuses = {
police = { label = "Night Shift Bonus", amount = 250, hourStart = 22, hourEnd = 6 },
ambulance = { label = "Emergency Response", amount = 300, minCalls = 5 },
}
Server-Side Logic
-- server.lua
local paychecks = {}
function CalculatePay(source, job)
local Player = QBCore.Functions.GetPlayer(source)
if not Player then return 0 end
local grade = Player.PlayerData.job.grade.level
local grades = Config.JobPayments[job]
if not grades then return 0 end
local pay = 0
for _, g in ipairs(grades) do
if g.grade == grade then
pay = g.amount
break
end
end
-- Apply bonuses
if Config.Bonuses[job] then
local bonus = Config.Bonuses[job]
local hour = GetClockHours()
if bonus.hourStart and bonus.hourEnd then
if hour >= bonus.hourStart or hour < bonus.hourEnd then
pay = pay + bonus.amount
end
end
end
return pay
end
-- Paycheck loop
Citizen.CreateThread(function()
while true do
Citizen.Wait(Config.PaycheckInterval)
local players = QBCore.Functions.GetPlayers()
for _, src in ipairs(players) do
local Player = QBCore.Functions.GetPlayer(src)
if Player and Player.PlayerData.job.onduty then
local job = Player.PlayerData.job.name
local amount = CalculatePay(src, job)
if amount > 0 then
Player.Functions.AddMoney("bank", amount, "job-paycheck")
TriggerClientEvent("qb-paycheck:notify", src, amount, job)
end
end
end
end
end)
Configurable Shop System
A shop system should be easy to configure without touching code. Here's a data-driven approach.
Item Definitions
-- shops/config.lua
Config.Shops = {
["weapon-shop"] = {
label = "Ammunation",
coords = vector3(22.09, -1104.44, 29.80),
blip = { sprite = 110, color = 1 },
job = nil,
items = {
{ name = "weapon_pistol", label = "Pistol", price = 5000, stock = 10, type = "weapon" },
{ name = "ammo-9", label = "9mm Ammo Box", price = 200, stock = 50, type = "item" },
{ name = "weapon_knife", label = "Combat Knife", price = 800, stock = 15, type = "weapon" },
}
},
["convenience-store"] = {
label = "24/7 Market",
coords = vector3(24.46, -1346.67, 29.50),
blip = { sprite = 52, color = 2 },
job = nil,
items = {
{ name = "bread", label = "Bread", price = 5, stock = 50 },
{ name = "water", label = "Water Bottle", price = 3, stock = 100 },
{ name = "phone", label = "Mobile Phone", price = 250, stock = 10 },
{ name = "sim_card", label = "SIM Card", price = 10, stock = 30 },
}
},
["police-armory"] = {
label = "Police Armory",
coords = vector3(434.88, -982.40, 30.71),
blip = { sprite = 110, color = 3 },
job = "police",
items = {
{ name = "weapon_carbinerifle", label = "Carbine Rifle", price = 0, stock = 5, grade = 3 },
{ name = "weapon_stungun", label = "Taser", price = 0, stock = 15, grade = 0 },
{ name = "armor", label = "Body Armor", price = 0, stock = 20, grade = 0 },
}
},
}
Shop Interaction (qb-target)
-- shops/client.lua
local QTarget = exports['qb-target']
Citizen.CreateThread(function()
for id, shop in pairs(Config.Shops) do
QTarget:AddBoxZone(id, shop.coords, 2.0, 2.0, {
name = id,
heading = 0,
debugPoly = false,
}, {
options = {
{
type = "client",
event = "qb-shops:openShop",
icon = "fas fa-store",
label = "Open " .. shop.label,
job = shop.job,
}
},
distance = 2.5,
})
end
end)
RegisterNetEvent("qb-shops:openShop", function()
local shopId = GetCurrentShop() -- determined by the zone entered
TriggerServerEvent("qb-shops:requestItems", shopId)
end)
Player-Run Shops
Player-run shops add a whole new dimension. Players can buy storefronts, set their own prices, and manage inventory.
-- player-shops/server.lua
RegisterServerEvent("qb-playershops:purchaseStorefront")
AddEventHandler("qb-playershops:purchaseStorefront", function(shopId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
local cost = Config.PlayerShops[shopId].purchasePrice
local cash = Player.PlayerData.money["cash"]
if cash < cost then
TriggerClientEvent("QBCore:Notify", src, "You don't have enough cash", "error")
return
end
Player.Functions.RemoveMoney("cash", cost, "storefront-purchase")
-- Create shop owner record
MySQL.insert("INSERT INTO player_shops (shop_id, owner, label, created_at) VALUES (?, ?, ?, NOW())",
{ shopId, Player.PlayerData.citizenid, Config.PlayerShops[shopId].label })
TriggerClientEvent("QBCore:Notify", src, "Storefront purchased! Use /setprice to configure your items", "success")
end)
RegisterServerEvent("qb-playershops:setPrice")
AddEventHandler("qb-playershops:setPrice", function(itemName, price)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
MySQL.update("UPDATE player_shop_items SET price = ? WHERE owner = ? AND item = ?",
{ price, Player.PlayerData.citizenid, itemName })
end)
Money Transaction Logging
Always log transactions. It helps with dispute resolution and cheat detection.
-- logging/server.lua
RegisterServerEvent("qb-economy:logTransaction")
AddEventHandler("qb-economy:logTransaction", function(playerId, type, amount, reason)
MySQL.insert("INSERT INTO money_logs (player_id, type, amount, reason, timestamp) VALUES (?, ?, ?, ?, NOW())",
{ playerId, type, amount, reason })
end)
A simple admin UI can query this table to spot anomalies — a player receiving 50 paycheck events in one hour is probably exploiting.
Testing Your Economy
Before going live:
- Create a test character with known money values and verify AddMoney/RemoveMoney
- Run the paycheck loop for several intervals and confirm amounts are correct
- Buy and sell items in every shop to verify stock management
- Test edge cases — what happens when a player has zero bank balance? What when stock is empty?
Next Steps
- Add an ATM system using qb-target with withdraw/deposit
- Implement sales tax per shop or per item category
- Build a stock management UI for shop owners to restock
- Add invoice and transfer systems between players
Ziad is the lead developer of FiveForge. He builds AI systems that read codebases and get work done.
Stay updated
Get the latest AI development guides and FiveForge updates delivered to your inbox.
Build faster with FiveForge
An AI agent for complex work. Plan, build, debug, and automate projects — all from your desktop.