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.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
removeMoneyclient-side or trust a client-sent price. Force every money operation onto the server keyed only offPROP_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
INSERTinto apropertiestable.
Integration Notes
- Needs a
propertiestable (property_id,owner,label,price). Create it before first run. - Declare
oxmysqlandes_extendedas dependencies infxmanifest.luaand 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), beginner transaction core priced just below the $50-389 band center against the demand-signal corpus tebex corpus (median ~$11.85K/mo); 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.
