Exports & Integration
Three things your script does to plug into jj-perks: award XP so players level up, and (optionally) read the level or check owned perks to apply a bonus. All of this is server-side through exports['jj-perks'].
source/src is the player server id. The category string must match the id you set in config.
Award XP
Call this whenever the player does the activity (sells ore, finishes a run, completes a heist). It handles levelling up and the level-up notification for you.
exports['jj-perks']:addXP(src, 'mining', 25)Always award from the server. XP is server-authoritative — there is no client-triggered XP event, so a cheat menu can't inject levels. If the activity finishes on the client, route it to your own server event and call the export there.
Apply a perk bonus
Ask whether the player owns a specific perk, then do whatever that perk promises. This is where the gameplay effect actually happens.
local amount = 1
if exports['jj-perks']:hasPerk(src, 'mine_r2') then -- "Double Drops I"
if math.random(100) <= 5 then amount = 2 end -- 5% chance, as the perk says
end
exports.ox_inventory:AddItem(src, 'iron_ore', amount)Gate something behind a level
local level = exports['jj-perks']:getPlayerLevel(src, 'mining') -- 0-10
if level < 5 then
-- not deep-mine ready yet
return
endgetPlayerProgress(src, 'mining') returns { level, xp, minXp, maxXp } if you want to show a progress bar somewhere.
Full example
A small mining handler that does all three:
RegisterNetEvent('my-mining:server:oreMined', function()
local src = source
-- 1. reward progression
exports['jj-perks']:addXP(src, 'mining', 25)
-- 2. faster gather if they own the speed perk (your timer reads this)
local gatherTime = 5000
if exports['jj-perks']:hasPerk(src, 'mine_r1') then
gatherTime = gatherTime * 0.98
end
-- 3. double drop chance from the drops perk
local amount = 1
if exports['jj-perks']:hasPerk(src, 'mine_r2') and math.random(100) <= 5 then
amount = 2
end
exports.ox_inventory:AddItem(src, 'iron_ore', amount)
end)Safety
If jj-perks might be stopped while your script runs, wrap the calls so a missing export can't error your resource:
local function ownsPerk(src, perk)
local ok, owned = pcall(function() return exports['jj-perks']:hasPerk(src, perk) end)
return ok and owned
endThat's the whole loop: define the category in config, call addXP when the activity happens, and check hasPerk / getPlayerLevel wherever you want the perk to actually do something. House Robbery is a working reference — it awards houserobbery XP on a completed contract and gates its MEDIUM/GARAGE/OFFICE/HARD/EXTREME tiers behind jjhr_r3, jjhr_r5 and jjhr_r6/jjhr_r10.
In-game admin tools
Open the tablet and switch to the Admin tab. From there you can add or remove XP, set a category to an exact level, grant or revoke individual perks, refund a category, and fully reset an online player. No commands or DB edits needed.
| Framework | Counts as admin |
|---|---|
| QBCore | admin/god permission, or ace command |
| QBox | ace command or group.admin |
| ESX | group admin/superadmin/mod, or ace command |
| ox_core | admin group, or ace command |
| ND_Core | ace command or group.admin |
Server exports
-- give XP (auto level-up + notify)
exports['jj-perks']:addXP(source, 'houserobbery', 50)
-- read current level (0-10)
local level = exports['jj-perks']:getPlayerLevel(source, 'houserobbery')
-- full progress: { level, xp, minXp, maxXp }
local p = exports['jj-perks']:getPlayerProgress(source, 'houserobbery')
-- does the player own a perk?
local owned = exports['jj-perks']:hasPerk(source, 'jjhr_r3')
-- list every unlocked perk id
local perks = exports['jj-perks']:getPlayerPerks(source)
-- admin tooling, both return (ok, newXp|errMsg)
exports['jj-perks']:setLevel(source, 'houserobbery', 5)
exports['jj-perks']:removeXP(source, 'houserobbery', 100)Client-side XP can also be awarded via the server event:
TriggerServerEvent('jj-perks:server:addXP', 'houserobbery', 25)Client exports
For embedding the perk system into another UI (a phone app, tablet, etc.):
-- open the tablet (also the ox_inventory item export 'jj-perks.openTablet')
exports['jj-perks']:openTablet()
-- the same payload the tablet NUI receives: config, levels, maxLevel, playerData, isAdmin, branding
local ui = exports['jj-perks']:getPerksUiData()
-- unlock a perk for the calling player (runs the same server validation)
exports['jj-perks']:externalUnlockPerk('jjhr_r3')
-- refund a category for the calling player
exports['jj-perks']:externalRefundPerks('houserobbery')When integrating from another resource, wrap calls in pcall() so a dependent doesn't error if jj-perks is stopped.
Database
Three tables, created on first boot:
jj_perks_playersjj_perks_unlockedjj_perks_category_xp
An optional install.sql is included if you'd rather create them yourself. The citizenid column holds whatever per-character id your framework uses.