DEV Community

Tack k
Tack k

Posted on

Building a Police Reward Distribution System with PED Interaction in QBCore

The scenario

In our QBCore RP server, police officers would arrest suspects and collect fines. But splitting that money fairly among everyone who responded to the call was always a manual headache — someone had to calculate the split and do individual transfers.

I wanted a system where an officer could walk up to a specific NPC, enter the total amount, select which colleagues participated, and have the money split and distributed automatically.

The result: lokat_sharing_money — a PED-based fund distribution script with job authorization, grade checks, on-duty filtering, and dual banking support.


How it works

Step 1 — Interact with the PED. A security guard NPC is placed at the police station. Officers approach and interact via qb-target. The canInteract check runs client-side to hide the option from unauthorized players.

canInteract = function()
    local PD = QBCore.Functions.GetPlayerData()
    local job   = PD.job.name
    local grade = PD.job.grade.level

    local okJob = false
    for _, j in ipairs(Config.AuthorizedJobs) do
        if j == job then okJob = true; break end
    end
    if not okJob then return false end

    local need = Config.MinGradeToStart[job]
    if need and grade < need then return false end

    if Config.RequireOnDutyToStart and not PD.job.onduty then
        return false
    end
    return true
end
Enter fullscreen mode Exit fullscreen mode

Step 2 — Enter the amount. A qb-input dialog asks for the total amount to distribute.

Step 3 — Select recipients. The server returns a list of online players with the same job who are currently on-duty. The officer selects who participated using a checkbox-style qb-menu.

-- Server: filter same job, on-duty players
for _, pid in ipairs(players) do
    local P = QBCore.Functions.GetPlayer(pid)
    if P then
        local dutyOK = (not Config.OnDutyOnly) or P.PlayerData.job.onduty
        if dutyOK and P.PlayerData.job.name == myJob then
            table.insert(list, { id = pid, name = fn .. " " .. ln })
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

Step 4 — Confirm and distribute. A summary screen shows the total and per-person amount. On confirm, the server deducts from the initiator's bank and distributes equally to each selected officer.

local perAmount = math.floor(total / #selectedPlayers)

for _, player in ipairs(selectedPlayers) do
    local receiver = QBCore.Functions.GetPlayer(tonumber(player.id))
    if receiver then
        receiver.Functions.AddMoney("bank", perAmount, "job-distribution-received")
    end
end
Enter fullscreen mode Exit fullscreen mode

Key design decisions

Job is inferred server-side. The client never sends which job to filter by — the server reads the initiator's job directly. This prevents players from spoofing a different job to access the distribution pool.

On-duty check at distribution time. Even if someone was selected as a recipient, the server re-checks their on-duty status at payment time. If they clocked out between selection and confirmation, they get skipped.

Dual banking support. Works with both qb-banking and okokBanking. When okokBanking is selected, a transaction log entry is also added for each recipient.

Config.BankingSystem = "okokBanking"  -- or "qb-banking"
Enter fullscreen mode Exit fullscreen mode

Multi-language support. All UI strings are defined in a locale table. Switch between Japanese and English with a single config value.

Config.Locale = "en"  -- or "ja"
Enter fullscreen mode Exit fullscreen mode

Config overview

Config.AuthorizedJobs = { 'police', 'ambulance', 'mechanic' }

Config.MinGradeToStart = {
    police    = 1,  -- sergeant and above
    ambulance = 1,
    mechanic  = 0,  -- any grade
}

Config.OnDutyOnly = true
Config.RequireOnDutyToStart = true

Config.Peds = {
    { model = "s_m_m_security_01",
      coords = vector4(438.82, -991.98, 28.69, 215.72),
      distance = 2.5 }
}
Enter fullscreen mode Exit fullscreen mode

Built with AI, designed by a non-coder

Like everything in this series, I designed the UX flow and logic, and implemented it with Claude Opus as my coding partner. The tricky parts — server-side job inference, double on-duty checks, banking abstraction — all came from thinking through edge cases before writing a single line.

Next up: Vol.3 — Building an In-Server Arcade with Tetris, Breakout, and Space Invaders in FiveM.


Questions or want the full source? Drop a comment below.

Top comments (0)