Tutorial 2026-04-07

FiveM Taxi & Transport Job Development

OntelMonke

OntelMonke

Admin & Developer at Agency Scripts

Taxi Job System Architecture

A taxi job system is a cornerstone feature for any roleplay server that wants to give players an accessible entry-level job with genuine player interaction. Unlike many other jobs that can feel grindy and repetitive, a well-designed taxi system creates organic roleplay encounters as drivers pick up passengers, negotiate fares, and navigate the city under time pressure. The architecture splits into two distinct modes: player-to-player transport where real passengers request rides, and NPC mission mode where the system generates pickup and dropoff locations for solo play during off-peak hours. Both modes share the same fare calculation engine, taxi meter UI, and vehicle management system. The key design decision is whether to use a centralized dispatch system where ride requests go into a shared queue visible to all on-duty drivers, or a proximity-based system where the nearest driver gets priority. Most successful implementations use a hybrid approach with a dispatch board that shows all pending requests but highlights nearby ones, giving drivers the freedom to choose their jobs while still ensuring passengers get picked up quickly.

Taxi Meter Implementation

The taxi meter is the heart of the fare system and needs to feel authentic while remaining transparent to both driver and passenger. A proper meter tracks two components: a base fare charged at the start of each ride, and a per-distance rate that ticks up as the vehicle moves. Some servers also add a waiting time rate that charges a lower per-second fee when the taxi is stationary, simulating the real-world practice of charging for time spent in traffic. The meter should be visible to both the driver and any passengers in the vehicle through a persistent NUI element positioned in the corner of the screen. Here is a client-side meter implementation that calculates fare based on distance traveled:

local meterActive = false
local currentFare = 0.0
local lastPosition = nil
local baseFare = 50
local ratePerMile = 15
local waitRatePerSecond = 0.5
local totalDistance = 0.0

function StartMeter()
    meterActive = true
    currentFare = baseFare
    totalDistance = 0.0
    lastPosition = GetEntityCoords(PlayerPedId())

    SendNUIMessage({action = 'showMeter', fare = currentFare, distance = 0.0})

    CreateThread(function()
        while meterActive do
            Wait(500)
            local currentPos = GetEntityCoords(PlayerPedId())
            local speed = GetEntitySpeed(PlayerPedId())

            if speed > 1.0 then
                local dist = #(currentPos - lastPosition)
                totalDistance = totalDistance + dist
                local miles = totalDistance / 1609.34
                currentFare = baseFare + (miles * ratePerMile)
            else
                currentFare = currentFare + (waitRatePerSecond * 0.5)
            end

            lastPosition = currentPos

            SendNUIMessage({
                action = 'updateMeter',
                fare = math.floor(currentFare),
                distance = math.floor(totalDistance),
                speed = math.floor(speed * 2.236936)
            })
        end
    end)
end

function StopMeter()
    meterActive = false
    local finalFare = math.floor(currentFare)
    SendNUIMessage({action = 'stopMeter', fare = finalFare})
    return finalFare
end

The NUI component for the meter should be compact but readable, showing the current fare prominently along with distance traveled and the current rate being applied. Use a monospace font for the fare display to mimic the look of a real taxi meter, and add a subtle ticking animation each time the fare increments to give visual feedback that the meter is running. Consider adding a fare estimate feature that calculates the approximate cost based on the straight-line distance to the destination before the ride starts, so passengers can decide if they want to proceed.

NPC Pickup Mission System

NPC missions keep taxi drivers busy when there are not enough real passengers on the server. The system generates random pickup and dropoff locations from a curated list of points of interest around the map, creates an NPC at the pickup location, and guides the driver through the complete fare cycle. The quality of the NPC mission experience depends heavily on the location data: using only random coordinates produces unnatural pickups in the middle of nowhere, so maintaining a hand-curated list of realistic locations like hospitals, restaurants, hotels, and shopping centers makes the missions feel genuine. Here is the server-side logic for generating and dispatching NPC missions:

local MissionLocations = {
    {coords = vector3(-240.49, -987.78, 29.29), label = 'Pillbox Medical Center'},
    {coords = vector3(228.28, -790.70, 30.58), label = 'Legion Square'},
    {coords = vector3(-1044.57, -2739.76, 13.85), label = 'Los Santos Airport'},
    {coords = vector3(126.19, -1282.72, 29.27), label = 'Strawberry Avenue'},
    {coords = vector3(-556.18, -196.17, 38.22), label = 'Rockford Hills'},
    {coords = vector3(1159.56, -314.45, 69.21), label = 'Vinewood Hills'},
    {coords = vector3(-1222.08, -906.82, 12.32), label = 'Del Perro Pier'},
    {coords = vector3(373.58, 325.71, 103.57), label = 'Downtown Vinewood'},
}

RegisterNetEvent('taxi:server:requestNPCMission', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player or Player.PlayerData.job.name ~= 'taxi' then return end

    local pickup = MissionLocations[math.random(#MissionLocations)]
    local dropoff = MissionLocations[math.random(#MissionLocations)]

    while pickup.label == dropoff.label do
        dropoff = MissionLocations[math.random(#MissionLocations)]
    end

    local estimatedDistance = #(pickup.coords - dropoff.coords)
    local basePay = 50
    local distancePay = math.floor(estimatedDistance / 1609.34 * 15)
    local totalPay = basePay + distancePay

    TriggerClientEvent('taxi:client:startNPCMission', src, {
        pickup = pickup,
        dropoff = dropoff,
        estimatedPay = totalPay,
        npcModel = Config.NPCModels[math.random(#Config.NPCModels)],
        missionId = 'TAXI-' .. os.time() .. '-' .. math.random(1000, 9999)
    })
end)

On the client side, spawn the NPC at the pickup location with a waiting animation, add a blip to guide the driver, and play an enter-vehicle animation when the driver arrives. During the ride, the NPC should use natural behaviors like looking around and occasionally commenting on the driving through chat bubbles. When the vehicle arrives at the destination, the NPC exits, and the fare is paid automatically through a server event. Add variety by occasionally generating special missions with higher payouts, like airport transfers or VIP clients who demand careful driving without any collisions.

Fare Calculation and Surge Pricing

Beyond the basic distance-based calculation, a sophisticated fare system accounts for time of day, traffic conditions, and vehicle type. Implement surge pricing during peak hours on the server when many players are online and taxi demand exceeds supply. The surge multiplier should be communicated transparently to passengers before they confirm the ride so there are no surprises at the end. You can calculate demand dynamically by tracking the ratio of pending ride requests to available drivers. Here is a fare calculation engine that incorporates these variables:

function CalculateFare(pickupCoords, dropoffCoords, vehicleClass)
    local distance = #(pickupCoords - dropoffCoords)
    local miles = distance / 1609.34

    local rates = Config.FareRates
    local baseFare = rates.base
    local perMile = rates.perMile

    -- Vehicle class multiplier
    local classMultiplier = {
        [0] = 0.8,   -- Compacts (budget)
        [1] = 1.0,   -- Sedans (standard)
        [2] = 1.0,   -- SUVs
        [3] = 1.2,   -- Coupes (comfort)
        [18] = 1.5,  -- Super (luxury)
    }
    local vehMultiplier = classMultiplier[vehicleClass] or 1.0

    -- Time-of-day multiplier (night premium)
    local hour = GetClockHours()
    local timeMultiplier = 1.0
    if hour >= 22 or hour <= 5 then
        timeMultiplier = 1.3  -- 30% night surcharge
    end

    -- Surge pricing based on demand
    local surgeMultiplier = GetCurrentSurgeMultiplier()

    local totalFare = (baseFare + (miles * perMile)) * vehMultiplier * timeMultiplier * surgeMultiplier

    return {
        total = math.floor(totalFare),
        breakdown = {
            base = baseFare,
            distance = math.floor(miles * perMile),
            vehicleClass = vehMultiplier,
            nightSurcharge = timeMultiplier > 1.0,
            surgeActive = surgeMultiplier > 1.0,
            surgeRate = surgeMultiplier
        }
    }
end

Display the fare breakdown to the passenger when they request a ride so they understand what they are paying for. If surge pricing is active, show the multiplier clearly with a different color indicator in the UI. This transparency builds trust between drivers and passengers and reduces disputes over fare amounts. Allow passengers to select their preferred vehicle class when booking, with budget options using compact cars and luxury options using high-end sedans or SUVs at premium rates.

Route Display and GPS Navigation

Providing visual route guidance makes the taxi experience smoother for both new and experienced drivers. FiveM provides the SetNewWaypoint native for basic GPS routing, but a more polished approach draws the route directly on the minimap and optionally renders a 3D path in the world. Use the SetBlipRoute function on destination blips to enable the built-in GPS path, and enhance it with custom blips at key waypoints along the route. For the passenger's benefit, display the estimated time of arrival and remaining distance in the meter UI, updating in real time as the vehicle progresses. Here is how to set up the route display system with waypoint tracking:

local routeBlip = nil
local checkpointBlips = {}

function SetTaxiRoute(destination, checkpoints)
    ClearTaxiRoute()

    routeBlip = AddBlipForCoord(destination.x, destination.y, destination.z)
    SetBlipSprite(routeBlip, 198)
    SetBlipColour(routeBlip, 5)
    SetBlipScale(routeBlip, 1.0)
    SetBlipRoute(routeBlip, true)
    SetBlipRouteColour(routeBlip, 5)
    BeginTextCommandSetBlipName('STRING')
    AddTextComponentSubstringPlayerName('Destination')
    EndTextCommandSetBlipName(routeBlip)

    if checkpoints then
        for i, cp in ipairs(checkpoints) do
            local cpBlip = AddBlipForCoord(cp.x, cp.y, cp.z)
            SetBlipSprite(cpBlip, 1)
            SetBlipColour(cpBlip, 3)
            SetBlipScale(cpBlip, 0.6)
            table.insert(checkpointBlips, cpBlip)
        end
    end
end

function ClearTaxiRoute()
    if routeBlip then
        SetBlipRoute(routeBlip, false)
        RemoveBlip(routeBlip)
        routeBlip = nil
    end
    for _, blip in ipairs(checkpointBlips) do
        RemoveBlip(blip)
    end
    checkpointBlips = {}
end

function UpdateETADisplay(destination)
    CreateThread(function()
        while routeBlip do
            Wait(2000)
            local pos = GetEntityCoords(PlayerPedId())
            local remaining = #(pos - destination)
            local speed = GetEntitySpeed(PlayerPedId())

            local etaSeconds = speed > 2.0 and (remaining / speed) or 0
            local etaMinutes = math.floor(etaSeconds / 60)

            SendNUIMessage({
                action = 'updateETA',
                distance = math.floor(remaining),
                eta = etaMinutes > 0 and (etaMinutes .. ' min') or 'Arriving'
            })
        end
    end)
end

For an extra layer of immersion, implement a rating system where passengers rate their ride based on factors like travel time compared to the estimate, number of collisions during the trip, and whether the driver followed traffic laws. Display the driver's average rating on their taxi profile so passengers can see it before accepting the ride, creating an incentive for drivers to provide good service.

Tip System and Driver Ratings

A tip system adds a rewarding social mechanic that encourages drivers to provide excellent service. After the fare is paid and the passenger exits the vehicle, display a tip prompt to the passenger with preset options like 10%, 15%, 20%, or a custom amount. The tip goes directly to the driver on top of the base fare and any commission from the taxi company. Track cumulative tips per driver in the database to create leaderboards and unlock bonuses for top-rated drivers. On the server side, process tips through the same transaction system as fares to ensure consistency and prevent duplication. Drivers who maintain a high rating and tip average over time could unlock perks like access to premium vehicles, reduced company commission rates, or priority in the dispatch queue. Combine the tip system with a review system where passengers leave a brief text review visible to the driver, creating a feedback loop that improves the overall taxi service quality on the server.

Dispatch Board and Ride Requests

The dispatch board is the central hub where taxi drivers find their next job. Build it as an NUI panel accessible from the taxi company headquarters or through a hotkey while on duty. The board should display all pending ride requests sorted by proximity, with each entry showing the passenger name, pickup location, destination, estimated fare, and estimated distance. When a driver accepts a job, remove it from the board for all other drivers and start a countdown timer, if the driver does not reach the pickup within the time limit, the job returns to the board and the driver receives a reliability penalty. For the passenger-facing side, create a simple phone app or command that lets them request a ride by specifying their current location and destination. The request enters the dispatch queue and the passenger receives updates as a driver accepts and approaches. Implement a cancellation system with a small fee for late cancellations to prevent passengers from repeatedly requesting and canceling rides, which wastes driver time and creates a frustrating experience.

Vehicle Management and Fleet System

A complete taxi system needs proper vehicle management so drivers use appropriate vehicles and the company maintains its fleet. Create a vehicle checkout system at the taxi depot where drivers select from available company vehicles. Each vehicle should have a tracked mileage counter, fuel level, and condition status, and drivers are responsible for returning vehicles in acceptable condition or facing repair deductions from their earnings. The depot should stock different vehicle tiers: economy sedans for standard fares, comfortable SUVs for premium service, and luxury vehicles for VIP bookings. Track each vehicle's usage statistics including total fares completed, revenue generated, and maintenance costs. Implement a fuel consumption system that requires drivers to refuel at gas stations during their shift, adding a realistic operational cost that experienced drivers learn to manage efficiently. At the end of a shift, the driver returns the vehicle to the depot, their total earnings are calculated minus the company commission and any vehicle damage costs, and the net pay is deposited into their bank account. This complete loop creates a realistic job experience that mirrors actual taxi company operations.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.