Install & integrate
Everything you need to drop account_levels into a FiveM server — requirements, install, framework detection, configuration, exports, lifecycle events, admin panel and the five canonical integration recipes.
Requirements
| Dependency | Used for |
|---|---|
| FiveM server | OneSync recommended (Infinity). |
oxmysql ≥ 2.7 | All database I/O. MariaDB 10.11 LTS+ or MySQL 8.x. |
ox_lib ≥ 3.0 | lib.addCommand, lib.callback, lib.notify. |
| Lua 5.4 | Enabled in fxmanifest.lua of dependent resources that call the exports. |
(Optional) qbx_core / qb-core / es_extended | Money + identity. Without one, runs standalone. |
(Optional) ox_inventory | Item rewards dispatched through it if present. |
Installation
- Drop the resource. Anywhere in your
resources/path — the structure assumesresources/[standalone]/account_levels/but it isn't required. - Order the ensures. Dependencies must start first in
server.cfg. - (Optional) Grant the admin ACE so staff can open the Admin tab and run
/alboost,/algrant. - Restart the server. The schema in
sql/install.sqlis loaded automatically — three tables prefixedals_will appear in your database.
ensure oxmysql
ensure ox_lib
ensure account_levels
add_ace group.admin account_levels.admin allow
Connect, press PageUp (or run /level) — the status panel opens. As an admin you'll see the ADMIN tab in the panel nav bar; non-admins don't.
Run /playtime in chat — you should see your tier. Press F7 — the status overlay should pop up. As admin, press F4 for the admin panel.
Framework bridge
Detection priority (configurable via Config.framework_priority). Override permanently with Config.framework = 'qbox' / 'qb-core' / 'esx' / 'standalone'.
| Order | Resource flag | Mode |
|---|---|---|
| 1 | qbx_core started | qbox |
| 2 | qb-core started (and qbx not present) | qb-core |
| 3 | es_extended started | esx |
| 4 | (none) | standalone |
Every server-side code path that touches the framework goes through a single bridge table (Framework.get_player, Framework.get_identifier, Framework.add_money, Framework.add_item, Framework.notify, Framework.on_player_loaded). To bridge another framework, drop a file in bridge/<name>.lua calling Framework.register('<name>', impl).
Config reference
Every knob lives in config.lua. The most-used groups follow.
Core
| Key | Default | Description |
|---|---|---|
Config.framework | 'auto' | 'auto' / 'qbox' / 'qb-core' / 'esx' / 'standalone' |
Config.primary_identifier | 'license' | Identifier that becomes the account_id. |
Config.table_prefix | 'als_' | Prefix for all three SQL tables. |
Config.locale | 'en' | Locale file (en / hu or your own). |
Config.debug | false | Emit [debug] lines. |
Ticker, cap, AFK
| Key | Default | Description |
|---|---|---|
tick_interval_seconds | 60 | How often the server adds a minute to active players. |
flush_interval_seconds | 300 | Batched DB flush — immediate on disconnect. |
daily_cap_minutes | 960 | Daily cap (16h). Set 0 to disable. |
timezone_offset_hours | 0 | Add to UTC for daily reset bucket. |
afk_timeout_seconds | 300 | Required input activity window. |
afk_position_threshold_meters | 5.0 | Server-side position cross-check. |
afk_grace_in_vehicle | true | Riding counts as active. |
Tier table
Config.levels = {
{ threshold_minutes = 0, title_key = 'tier_0' },
{ threshold_minutes = 480, title_key = 'tier_1' },
{ threshold_minutes = 960, title_key = 'tier_2' },
{ threshold_minutes = 1920, title_key = 'tier_3' },
{ threshold_minutes = 3840, title_key = 'tier_4' },
{ threshold_minutes = 7680, title_key = 'tier_5' },
{ threshold_minutes = 15360, title_key = 'tier_6' },
{ threshold_minutes = 30720, title_key = 'tier_7' },
{ threshold_minutes = 61440, title_key = 'tier_8' }
}
Add or remove rows freely. threshold_minutes is in active minutes; title_key maps to a locale entry.
Rewards on level-up
Config.level_up_rewards = {
[1] = { money = 0, items = {}, custom_event = nil },
[2] = { money = 2500, items = { { name = 'phone', count = 1 } }, custom_event = nil },
[3] = { money = 5000, items = {}, custom_event = 'myresource:onCitizen' },
}
Config.reward_money_account = 'bank' -- 'cash' / 'bank' / framework-specific
Progression modifiers
Config.boost_groups = {
['group.donator'] = 1.2,
['group.vip'] = 1.5
}
Config.event_boost = {
enabled = true,
default_multiplier = 1.0,
max_multiplier = 5.0,
max_duration_hours = 168
}
Config.catchup_bonus = { enabled = false, duration_hours = 24, multiplier = 1.5 }
Config.decay = { enabled = false, inactive_days = 30, minutes_per_day = 2, floor_level = 1 }
Multipliers stack multiplicatively. Daily cap is still enforced.
Ped preview camera
The 3D character render in the status panel uses a scripted camera positioned in the player's local frame. Tune live with /alpreviewdebug.
Config.ped_preview = {
enabled = true,
rotate_speed = 0.0,
cam_offset_x = -0.84,
cam_offset_y = 2.20,
cam_offset_z = -0.24,
aim_offset_x = 1.20,
aim_offset_y = -0.66,
aim_offset_z = 0.60,
fov = 38.0,
dof_near = 3.0,
dof_far = 17.6,
dof_blur_strength = 6.5,
hide_radar = true,
hide_weapon = true
}
Commands & keybinds
One unified panel with tab navigation. Open it with PageUp — the Admin tab is hidden if the player doesn't match Config.admin_ace.
| Action | Default |
|---|---|
| Toggle panel | PageUp |
| Switch tab | Click STATUS / LEADERBOARD / ADMIN |
| Close panel | Escape or PageUp |
| Dismiss level-up animation | Space (or auto-close) |
| Command | Purpose |
|---|---|
/level | Open the panel on the Status tab |
/level leaderboard | Open straight to Leaderboard |
/level admin | Open straight to Admin (ACE gated) |
/leaderboard | Standalone shortcut to Leaderboard |
/aladmin | Standalone shortcut to Admin |
/playtime | Print playtime + tier in chat |
/alpreview [tier] | Replay the level-up animation (admin) |
/alboost <multiplier> <hours> [label] | Start a server-wide XP event (admin) |
/algrant <playerId> <minutes> [reason] | Grant bonus minutes (admin) |
/alpreviewdebug | Open the live ped preview tuning panel (admin) |
Exports (server API)
Every export is on the server.
exports.account_levels:GetLevel(source) -- int (0..max_level)
exports.account_levels:GetTotalMinutes(source) -- int
exports.account_levels:GetProgressToNext(source) -- { current, target, percent }
exports.account_levels:RequireLevel(source, n) -- bool; sends a notification on failure
exports.account_levels:GetAccountId(source) -- string
exports.account_levels:GetTierTitle(level) -- string (locale-aware)
exports.account_levels:GrantBonusMinutes(src, m, r) -- bool; audit logged
exports.account_levels:GetLeaderboard(limit, offset) -- array of rows
exports.account_levels:RegisterGate(id, opts) -- opts = { level, message, title_key }
exports.account_levels:CheckGate(id, source) -- bool
exports.account_levels:GetStatusPayload(source) -- full state table
exports.account_levels:GetEventBoost() -- { multiplier, remaining_seconds, label }
RequireLevel rejects with a localized notification on fail, so you don't have to handle UX in your own resource.
Lifecycle events
| Event | Args |
|---|---|
account_levels:onPlayerLoaded | (source, { account_id, level, total_minutes, is_new }) |
account_levels:onMinuteTicked | (source, total_minutes, is_capped) |
account_levels:onLevelUp | (source, oldLevel, newLevel, total_minutes) |
account_levels:onDailyReset | (source, account_id) |
AddEventHandler('account_levels:onLevelUp', function(source, oldLvl, newLvl, total)
if newLvl == 5 then
TriggerClientEvent('myresource:veteranReached', source)
end
end)
Database schema
Three tables prefixed als_ (configurable). InnoDB, utf8mb4_unicode_ci. The schema in sql/install.sql is loaded automatically on resource start.
als_accounts
Primary table — one row per account. Includes account_id, identifier_kind, display_name, secondary_identifiers (JSON), total_minutes, level, prestige, daily_minutes, daily_key, weekly_minutes, weekly_key, sessions, last_session_minutes, first_seen, last_seen, created_at, updated_at. Indexes on level, total_minutes, last_seen.
als_level_history
Append-only history of every tier change (including admin-driven). id, account_id, from_level, to_level, total_minutes, created_at.
als_admin_log
Audit log for every admin action: id, target_account_id, staff_identifier, staff_name, action (ADD_MINUTES / SET_TIER / GRANT_PRESTIGE / RESET / GRANT_BONUS), payload (JSON), reason, created_at.
Admin panel
Open with PageUp and the ADMIN tab (or /level admin / /aladmin). Visible only if the player matches any ACE in Config.admin_ace.
| Action | Input | Effect |
|---|---|---|
| Add minutes | minutes, reason | total_minutes += minutes; re-evaluates level for online users |
| Set tier | tier (0..max), reason | Forces the tier; raises total_minutes to threshold if needed |
| Grant prestige | amount, reason | Increments the prestige counter |
| Reset account | reason | Zeroes minutes, level and prestige |
Every action triggers a Discord post if Config.discord_announce_admin_actions = true.
Progression modifiers
Stack multiplicatively, daily cap is always enforced last.
- Event boost — server-wide multiplier via
/alboost <multiplier> <hours> [label]. Capped bymax_multiplierandmax_duration_hours. - Per-ACE-group multiplier —
Config.boost_groups = { ['group.donator'] = 1.2 }. Highest match wins. - Catchup bonus — new accounts get a multiplier for the first N hours.
- Decay — slowly reduces minutes for accounts inactive for
inactive_days+. Floors atfloor_level.
Discord webhook
Set Config.discord_webhook_url, then opt in to the announce flags. Empty URL is a no-op.
Config.discord_webhook_url = ''
Config.discord_webhook_username = 'Account Levels'
Config.discord_webhook_avatar = ''
Config.discord_announce_levelups = false
Config.discord_announce_admin_actions = false
Level-up embeds are amber; admin-action embeds are magenta.
Integration recipes
Five worked examples — copy/paste/adjust.
1. Job whitelist (qbox / qb-core)
RegisterNetEvent('myjobs:server:setJob', function(jobName)
local src = source
if jobName == 'police' and not exports.account_levels:RequireLevel(src, 3) then
return
end
TriggerClientEvent('QBCore:Notify', src, 'Job set.')
end)
2. Vehicle dealer (declarative gate)
exports.account_levels:RegisterGate('dealer_supercar', {
level = 5,
message = 'Supercars require Veteran status.'
})
RegisterNetEvent('dealer:server:buy', function(model)
local src = source
if model == 'turismor' then
if not exports.account_levels:CheckGate('dealer_supercar', src) then return end
end
end)
3. Property purchase (per-property minimum)
local MIN_TIER_BY_PROPERTY = {
['vinewood-1'] = 6,
['paleto-cabin'] = 0,
}
local function can_buy(src, property_id)
local required = MIN_TIER_BY_PROPERTY[property_id] or 0
if required == 0 then return true end
return exports.account_levels:RequireLevel(src, required)
end
4. Faction join (welcome + gate)
AddEventHandler('account_levels:onPlayerLoaded', function(source, data)
if data.is_new then
TriggerClientEvent('chat:addMessage', source, {
args = { 'Welcome', 'New account · play 8 hours to reach Tier 1.' }
})
end
end)
RegisterNetEvent('factions:server:join', function(faction_id)
local src = source
if faction_id == 'thelostmc' and not exports.account_levels:RequireLevel(src, 4) then return end
end)
5. Weapon gate (ox_inventory hook)
exports.account_levels:RegisterGate('heavy_weapons', {
level = 6,
message = 'Heavy weapons require Respected status.'
})
local restricted = {
WEAPON_RPG = true, WEAPON_MINIGUN = true, WEAPON_GRENADELAUNCHER = true,
}
exports.ox_inventory:registerHook('createItem', function(payload)
local item = (payload.item and payload.item.name) or ''
if not restricted[item:upper()] then return true end
return exports.account_levels:CheckGate('heavy_weapons', payload.inventoryId)
end)
Bonus — scheduled XP weekend
-- every Friday at 18:00 server time, start a 2× boost for 60 hours
CreateThread(function()
while true do
local hour = tonumber(os.date('%H')) or 0
local wday = tonumber(os.date('%w')) or 0
if wday == 5 and hour == 18 then
ExecuteCommand('alboost 2 60 Weekend')
Wait(3600 * 1000)
end
Wait(60 * 1000)
end
end)
Troubleshooting
| Symptom | Fix |
|---|---|
oxmysql not started — account_levels will not function | Ensure oxmysql starts before account_levels. |
| Admin tab hidden for staff | Add your admin ACE to Config.admin_ace. Defaults accept account_levels.admin, admin, command, group.admin. |
| Status panel says "Action failed" | Open F8 console for the reason. Toast also includes the failure code (invalid_minutes, no_permission, not_found). |
| Character preview not visible | Confirm Config.ped_preview.enabled = true. Use /alpreviewdebug to dial in offsets. |
| Daily reset at the wrong hour | Set Config.timezone_offset_hours (e.g. 1 CET, 2 CEST). |
RequireLevel always returns false | The export is server-side. Ensure your handler runs on the server, not the client. |
The shipped README documents every config knob in line-by-line detail. Open a Tebex ticket if you hit something the docs don't cover.