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). [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.