Account Levels documentation

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.

← Back to documentation hub

Requirements

DependencyUsed for
FiveM serverOneSync recommended (Infinity).
oxmysql ≥ 2.7All database I/O. MariaDB 10.11 LTS+ or MySQL 8.x.
ox_lib ≥ 3.0lib.addCommand, lib.callback, lib.notify.
Lua 5.4Enabled in fxmanifest.lua of dependent resources that call the exports.
(Optional) qbx_core / qb-core / es_extendedMoney + identity. Without one, runs standalone.
(Optional) ox_inventoryItem rewards dispatched through it if present.

Installation

  1. Drop the resource. Anywhere in your resources/ path — the structure assumes resources/[standalone]/account_levels/ but it isn't required.
  2. Order the ensures. Dependencies must start first in server.cfg.
  3. (Optional) Grant the admin ACE so staff can open the Admin tab and run /alboost, /algrant.
  4. Restart the server. The schema in sql/install.sql is loaded automatically — three tables prefixed als_ 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.

Verify

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'.

OrderResource flagMode
1qbx_core startedqbox
2qb-core started (and qbx not present)qb-core
3es_extended startedesx
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

KeyDefaultDescription
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.debugfalseEmit [debug] lines.

Ticker, cap, AFK

KeyDefaultDescription
tick_interval_seconds60How often the server adds a minute to active players.
flush_interval_seconds300Batched DB flush — immediate on disconnect.
daily_cap_minutes960Daily cap (16h). Set 0 to disable.
timezone_offset_hours0Add to UTC for daily reset bucket.
afk_timeout_seconds300Required input activity window.
afk_position_threshold_meters5.0Server-side position cross-check.
afk_grace_in_vehicletrueRiding 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.

ActionDefault
Toggle panelPageUp
Switch tabClick STATUS / LEADERBOARD / ADMIN
Close panelEscape or PageUp
Dismiss level-up animationSpace (or auto-close)
CommandPurpose
/levelOpen the panel on the Status tab
/level leaderboardOpen straight to Leaderboard
/level adminOpen straight to Admin (ACE gated)
/leaderboardStandalone shortcut to Leaderboard
/aladminStandalone shortcut to Admin
/playtimePrint 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)
/alpreviewdebugOpen 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

EventArgs
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.

ActionInputEffect
Add minutesminutes, reasontotal_minutes += minutes; re-evaluates level for online users
Set tiertier (0..max), reasonForces the tier; raises total_minutes to threshold if needed
Grant prestigeamount, reasonIncrements the prestige counter
Reset accountreasonZeroes 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.

  1. Event boost — server-wide multiplier via /alboost <multiplier> <hours> [label]. Capped by max_multiplier and max_duration_hours.
  2. Per-ACE-group multiplierConfig.boost_groups = { ['group.donator'] = 1.2 }. Highest match wins.
  3. Catchup bonus — new accounts get a multiplier for the first N hours.
  4. Decay — slowly reduces minutes for accounts inactive for inactive_days+. Floors at floor_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

SymptomFix
oxmysql not started — account_levels will not functionEnsure oxmysql starts before account_levels.
Admin tab hidden for staffAdd 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 visibleConfirm Config.ped_preview.enabled = true. Use /alpreviewdebug to dial in offsets.
Daily reset at the wrong hourSet Config.timezone_offset_hours (e.g. 1 CET, 2 CEST).
RequireLevel always returns falseThe export is server-side. Ensure your handler runs on the server, not the client.
Need more?

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.