This guide walks you through designing and implementing a robust QBCore taxi job: server-side fare logic, secure events, qb-target pickup interactions, and installable resource files. If you want to scaffold the project faster, open the Stellar AI project planner in the app: https://trystellarai.com/app to create files from plain-English prompts.
Why server authority matters
Taxi jobs are straightforward gameplay but attractive for exploits: money transfers, fake distances, or client-side manipulation. Make the server authoritative for trip state (active trip, pickup time), fare computation, and payment. Keep clients responsible only for UI, local markers, and requesting server actions. This minimizes cheating while keeping responsive client UX.
Design principles and constraints
- Validate job permissions server-side: only players with job.name == 'taxi' may accept fares or be paid.
- Compute fare server-side using stored pickup/dropoff coordinates and a safe formula; clamp for sanity.
- Store active trips server-side with timestamps and coordinates to compute duration/distance.
- Use qb-target or ox_target on client to start interactions but call server events to register state.
Quick implementation plan
- Create resource skeleton (fxmanifest.lua, config.lua, client.lua, server.lua, html/UI if needed).
- Expose qb-target zones to start/stop trips and use client events to call server APIs.
- Server: Maintain activeTrips table, compute fares, validate passenger funds, and perform atomic payment operations.
- Test edge cases: disconnect mid-trip, Data persistence, and unusual coordinates.
If you'd like a fast scaffold generator, you can also create a plan inside Stellar AI: https://trystellarai.com/app and export complete files.
Files, placement, and responsibilities (practical checklist)
| Path | Purpose |
|---|---|
| qb-taxi/fxmanifest.lua | Resource manifest |
| qb-taxi/config.lua | Config: fares, clamp values, target models |
| qb-taxi/client.lua | qb-target interactions, blips, UI triggers |
| qb-taxi/server.lua | Active trip tracking, fare calc, payment, validation |
| qb-taxi/html/* | Optional UI (ride request panel) |
Example files and safe code snippets
fxmanifest to start:
fx_version 'cerulean'
games { 'gta5' }
author 'you'
description 'QBCore Taxi job'
version '1.0.0'
shared_script 'config.lua'
server_script 'server.lua'
client_script 'client.lua'
Server: authoritative trip completion and payment (QBCore conventions)
-- server.lua
local QBCore = exports['qb-core']:GetCoreObject()
local activeTrips = {} -- key: driverSource -> { passenger = id, pickup = vec3, started = os.time() }
RegisterNetEvent('qb-taxi:server:StartTrip', function(passengerId, pickupX, pickupY, pickupZ)
local src = source
local driver = QBCore.Functions.GetPlayer(src)
if not driver or driver.PlayerData.job.name ~= 'taxi' then return end
-- Basic validation of values
local pickup = { x = tonumber(pickupX) or 0, y = tonumber(pickupY) or 0, z = tonumber(pickupZ) or 0 }
activeTrips[src] = { passenger = passengerId, pickup = pickup, started = os.time() }
TriggerClientEvent('QBCore:Notify', src, 'Trip started', 'success')
end)
RegisterNetEvent('qb-taxi:server:CompleteTrip', function(dropX, dropY, dropZ)
local src = source
local trip = activeTrips[src]
if not trip then return end
local driver = QBCore.Functions.GetPlayer(src)
if not driver or driver.PlayerData.job.name ~= 'taxi' then
activeTrips[src] = nil
return
end
local passenger = QBCore.Functions.GetPlayer(tonumber(trip.passenger))
if not passenger then
activeTrips[src] = nil
return
end
-- Simple distance-based fare (server-side)
local dx = dropX - trip.pickup.x
local dy = dropY - trip.pickup.y
local distance = math.sqrt(dx*dx + dy*dy)
local baseFare = 50
local perMeter = 0.5
local fare = math.floor(baseFare + (distance * perMeter))
-- Clamp fare to server policy
fare = math.max(20, math.min(fare, 2000))
-- Check passenger funds and perform payment atomically
local success = false
if passenger.Functions.RemoveMoney then
-- remove from passenger then add to driver
local removed = passenger.Functions.RemoveMoney('bank', fare, 'taxi_fare')
if removed then
driver.Functions.AddMoney('bank', fare, 'taxi_income')
success = true
end
end
if success then
TriggerClientEvent('QBCore:Notify', src, 'Trip complete: $' .. fare, 'success')
TriggerClientEvent('QBCore:Notify', passenger.PlayerData.source, 'You paid $' .. fare .. ' for the ride', 'success')
else
TriggerClientEvent('QBCore:Notify', src, 'Trip failed: passenger could not pay', 'error')
end
activeTrips[src] = nil
end)
Client: qb-target example to start a trip on a vehicle or NPC. Keep it UI-only and call server events.
-- client.lua (snippet)
local QBCore = exports['qb-core']:GetCoreObject()
-- qb-target setup, run once
exports['qb-target']:AddTargetEntity(vehicleEntity, {
options = {
{ type = 'client', event = 'qb-taxi:client:RequestPickup', icon = 'fa fa-taxi', label = 'Request Taxi' }
}, distance = 3.5
})
RegisterNetEvent('qb-taxi:client:RequestPickup', function()
local ped = PlayerPedId()
local pos = GetEntityCoords(ped)
TriggerServerEvent('qb-taxi:server:RequestTaxi', pos.x, pos.y, pos.z)
end)
Validation and security checklist
- Only accept StartTrip/CompleteTrip from players whose server-side job == 'taxi'.
- Clamp fare to min/max; do not accept client-supplied fare amount.
- Verify passenger player is connected before trying to debit money.
- Handle disconnects by expiring activeTrips after a timeout server-side.
- Log suspicious events for manual review (excessive distance or repeated failures).
Roblox parity notes (server/client separation)
If you want a similar Taxi job on Roblox: keep the server authoritative. Use RemoteEvents for requests and server Scripts to store active rides. For persistent earnings, use DataStoreService with pcall and retries — never trust client-sent fares. Example: server Script listens to a RemoteEvent 'RequestTaxi', validates player and assigns driver, then when trip ends it updates the driver's leaderstats and writes to DataStore with safe error handling.
-- Roblox Server Script (snippet)
local Remote = game.ReplicatedStorage:WaitForChild('TaxiRemote')
local DataStore = game:GetService('DataStoreService'):GetDataStore('TaxiEarnings')
Remote.OnServerEvent:Connect(function(player, action, params)
if action == 'CompleteTrip' then
local fare = math.clamp(math.floor(params.distance * 0.5) + 50, 20, 2000)
-- Update leaderstats server-side
player.leaderstats.Cash.Value = player.leaderstats.Cash.Value + fare
-- Save earnings safely
local success, err = pcall(function()
DataStore:SetAsync('earnings_'..player.UserId, player.leaderstats.Cash.Value)
end)
if not success then warn('DataStore save failed', err) end
end
end)
Next steps
Test the resource in a development server, simulate edge cases (disconnects, insufficient funds), and iterate. If you prefer a prompt-driven scaffold that produces complete destination-labelled files, open the project planner in the Stellar AI app: https://trystellarai.com/app. For deeper reading on job patterns and server authority, see the Stellar AI blog: https://trystellarai.com/blog