gta6/prompts
housing
Freebeginnerhousing

Buyable Property + Ownership

Stand on a for-sale marker, press E to buy, and the server checks money, takes payment, and writes ownership to the database.

NON-TESTÉ — the reference Lua is syntax-validated, not run in a live FiveM server. Adapt and test on your own dev server before shipping.
Est. Lua
~70 loc
Claude
Claude Opus 4.x / Sonnet 4.x
Validation
syntax-validated
Updated
2026-06-24

Description

A complete buy flow for a single property: the player walks onto a for-sale marker, sees the price, and presses E to purchase. The server is authoritative — it checks the property is unowned, verifies the player can afford it, removes the money, and inserts the ownership row. This is the transaction core of any player-housing economy.

Prompt Template

You are writing a FiveM script for es_extended (stable, exports-based
getSharedObject) with oxmysql for persistence. Produce ONE Lua file with
"-- ===== client.lua =====" and "-- ===== server.lua =====" banners.

Goal: a buyable property.
- Property: id "[PROP_ID]", label "[PROP_LABEL]", price [PRICE], marker at
  [PROP_COORDS vector3].
- CLIENT: proximity loop (PlayerPedId / GetEntityCoords); within 1.5m show the
  price help text; on IsControlJustReleased(0, 38)
  TriggerServerEvent("housing:buyProperty", PROP_ID). Show the result via a
  "housing:buyResult" handler and ESX.ShowNotification.
- SERVER: on "housing:buyProperty" resolve xPlayer via
  ESX.GetPlayerFromId(source); look up the property in a SERVER-side table.
  Query the DB to confirm it is UNOWNED first, THEN check getMoney(), THEN
  removeMoney(), THEN MySQL.insert the ownership row. Send a success/failure
  back with TriggerClientEvent("housing:buyResult", src, ok, msg).

Requirements: ALL price and money logic on the SERVER (never trust the client);
check ownership in the DB before charging; persist with oxmysql, not a Lua
table; guard xPlayer for nil. Return only the Lua.

Expected Output

The reference Lua at content/expected-outputs/housing/02-property-for-sale.lua shows the client proximity prompt and a server handler that checks DB ownership, validates funds, charges, and inserts the row — all server-authoritative. Split into client.lua / server.lua in fxmanifest.lua.

02-property-for-sale.lua70 lines
-- 02-property-for-sale.lua
-- Buyable property: marker prompt -> server validates money + writes ownership.
-- ESX (exports getSharedObject) + oxmysql for persistence.

-- ===== client.lua =====
local ESX = exports["es_extended"]:getSharedObject()

local PROPERTY = {
    id    = "eclipse_3a",
    label = "Eclipse Towers 3A",
    price = 75000,
    coords = vector3(-773.45, 312.66, 85.70),
}

CreateThread(function()
    while true do
        local sleep = 1000
        local coords = GetEntityCoords(PlayerPedId())
        if #(coords - PROPERTY.coords) < 1.5 then
            sleep = 0
            BeginTextCommandDisplayHelp("STRING")
            AddTextComponentSubstringPlayerName(("Buy ~y~%s~s~ for ~g~$%d~s~ (~INPUT_CONTEXT~)"):format(PROPERTY.label, PROPERTY.price))
            EndTextCommandDisplayHelp(0, false, true, -1)
            if IsControlJustReleased(0, 38) then
                TriggerServerEvent("housing:buyProperty", PROPERTY.id)
            end
        end
        Wait(sleep)
    end
end)

RegisterNetEvent("housing:buyResult", function(ok, msg)
    ESX.ShowNotification(msg)
end)

-- ===== server.lua =====
local ESXs = exports["es_extended"]:getSharedObject()

local PROPERTIES = {
    eclipse_3a = { label = "Eclipse Towers 3A", price = 75000 },
}

RegisterNetEvent("housing:buyProperty", function(propId)
    local src = source
    local prop = PROPERTIES[propId]
    if not prop then return end

    local xPlayer = ESXs.GetPlayerFromId(src)
    if not xPlayer then return end

    -- Already owned by someone? Reject before touching money.
    MySQL.scalar("SELECT owner FROM properties WHERE property_id = ?", { propId }, function(owner)
        if owner then
            TriggerClientEvent("housing:buyResult", src, false, "That property is already owned.")
            return
        end
        if xPlayer.getMoney() < prop.price then
            TriggerClientEvent("housing:buyResult", src, false, "You cannot afford this property.")
            return
        end

        xPlayer.removeMoney(prop.price)
        MySQL.insert(
            "INSERT INTO properties (property_id, owner, label, price) VALUES (?, ?, ?, ?)",
            { propId, xPlayer.identifier, prop.label, prop.price }
        )
        TriggerClientEvent("housing:buyResult", src, true, ("Purchased %s!"):format(prop.label))
    end)
end)

Known Failure Modes

  • Client-side money check — Claude may call removeMoney client-side or trust a client-sent price. Force every money operation onto the server keyed only off PROP_ID.
  • Charge-before-ownership-check — removing money then discovering the property is taken. Order it: confirm unowned → check funds → charge → insert.
  • In-memory ownership — a Lua table loses everything on restart. Require an oxmysql INSERT into a properties table.

Integration Notes

  • Needs a properties table (property_id, owner, label, price). Create it before first run.
  • Declare oxmysql and es_extended as dependencies in fxmanifest.lua and load oxmysql first.
  • Test by buying once (should succeed), then again from a second character (should report "already owned").

Profit Potential

$175–$2400/mo on Tebex (expected ~$650). [INFERRED] beginner transaction core priced just below the $50-389 band center against the signal-scraper tebex corpus (median seller $11.85K/mo, n=100); a ubiquitous need lifts it slightly over a bare instancing script.

Trend Signal

rising — housing systems niche-selection 3.60; corpus ox_doorlock active.

Sales Angle

Position as the server-authoritative purchase core of any housing economy. Recommended Tebex price $59 — or bundle it with instancing as a $99 housing-starter that converts new server owners.

Difficulty & Ship Time

beginner · ships in 2-4h.