QBCoreJuly 14, 20266 min read

"Building a QBCore Economy System with Job Paychecks and Shops"

"A step-by-step guide to creating a complete economy system for QBCore servers — configurable job paychecks, player-run shops, and wallet-bank money management."

ZZiadLead Developer
Share

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.

lua
-- 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

lua
-- 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

lua
-- 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

lua
-- 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)

lua
-- 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.

lua
-- 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.

lua
-- 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:

  1. Create a test character with known money values and verify AddMoney/RemoveMoney
  2. Run the paycheck loop for several intervals and confirm amounts are correct
  3. Buy and sell items in every shop to verify stock management
  4. 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
ZiadLead Developer

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.