Configuration Reference

How Oxide Drugselling stores its settings, the in-game settings menu, and every setting — street selling, heat, witnesses, corners, the trap phone, clients, meets, dead drops, reputation, evidence, bulk buyers, stings, and the drugs file.

oxide-drugselling is configured in-game, from a settings menu. This page explains how that works, then lists every setting so you know what each one changes.

How Configuration Is Stored

oxide-drugselling keeps its live settings in your database, in a shared table called oxide_settings. The files in shared/ are the factory defaults: the values the resource ships with. The table is created automatically on first start.

  1. The first time the resource starts, the values from the config files are copied into oxide_settings. If you edited the files before that first start, your edits are what gets copied.
  2. From then on, the database is what the resource reads. Editing a config file on a server that has already started once does not change a setting that's stored in the database.
  3. When an update adds a new setting, it isn't in your database yet, so it's copied from the updated file on the next start. You don't need to do anything.

So: to change a setting on a running server, use the menu.

The in-game settings menu

As an admin, run /drugselling settings. Changes apply live and are saved to the database straight away, with no restart.

  • Settings are grouped into four categories in the sidebar: Core (General, Street, Heat & Witnesses, Corners), Phone (Phone, Clients, Orders & Meets, Dead Drops, Reputation), Pressure (Evidence, Bulk Buyers, Stings) and Advanced (Compatibility).
  • A Basic / Advanced switch at the top picks which settings you see. Basic shows the everyday settings. Advanced shows only Debug logging, Time of day, No-signal areas, Evidence bridges and Model swaps.
  • A search box finds a setting in the view you're on.
  • An edited but unsaved field is marked changed. Save Changes saves everything you edited.
  • Drop spots have a Place in world button: the menu hides, you pick the spot on the ground, and the menu comes back. Teleport here takes you to a spot.
  • No-signal areas are drawn in the world: Draw new zone, place the corners of the area, and confirm.
  • Item fields have a picker listing the items your inventory knows about.

/drugselling corners and /drugselling drops open the same menu straight on the Corners and Dead Drops sections. See Admin.

Almost every change takes effect the moment you save. A few things to know:

  • Turning Corners → Enabled off closes every corner currently being worked ("Corner selling was switched off"). Other corner changes apply the next time they're used, including on corners already being worked.
  • Renaming Corners → Command takes effect after a restart.
  • Renaming the phone, SIM, charger or sting phone item registers the new name live. The new item must exist in your inventory.
  • Saving a setting that fails a check (for example corners switched on with an empty Demand list) is refused, and the field is highlighted with the reason.

Developers can also read and write settings from another resource. See Exports → GetSetting.

The setting names below (like Config.Selling.minPoliceOnline) are the names in the config files. The menu shows a plain label instead (like "Police needed online"). The tables list both.

Before you press Reset to default

Reset to default puts a whole setting back to its value in the config files and saves it straight away. It resets everything in that setting:

  • Resetting Corners also resets its Demand list and its buyer model and vehicle lists.
  • Resetting Dead drops replaces your drop spots with the 10 default spots.
  • Resetting No-signal areas deletes every area you drew.

"Default" means the config file as it was when the server last started.

File-Only Settings

These are never stored in the database. The config file always wins. To change one: edit the file, save it, and restart the server.

SettingFileWhat it is
Config.Drugsshared/config/drugs.luaEvery sellable drug and how it's priced. See Drugs
Config.OnDrugSoldshared/config.luaYour own code that runs after every sale
Config.OnPhoneExaminedshared/config.luaYour own code that runs when an officer opens a seized phone
Config.SettingsCommandshared/config.luaThe command named in missing-model warnings ('/drugselling settings')
Config.ModelHintsshared/config.luaMarks models that ship with the resource or come from an add-on, for the missing-model warning. None ship

A few values sit inside a setting the menu shows, but aren't shown themselves. They're marked (not in menu) in the tables below, and they're read from the config file on every start: Config.Selling.targetIcon, Config.Selling.targetLabel and Config.Selling.anims. (If a saved setting ever fails its check when the server starts, it's loaded exactly as saved and these file values are ignored until you save it again in the menu.)

Sale hook

Config.OnDrugSold runs on the server after every completed sale, whichever way it was made (for a dead drop, when the client collects the package). Leave it empty to do nothing. It gets one table:

FieldTypeWhat it is
sourcenumberThe seller's server ID
charIdstringThe seller's character identifier
drugTypestringThe drug's key in Config.Drugs (for example weed)
itemNamestringThe item sold
quantitynumberUnits sold
xpUnitsnumberProgression units for this sale: quantity times the item's xpUnits, or quantity when that row leaves it out
finalPricenumberThe sale price, after any gang territory bonus. For a dead drop, the cash paid or left at the drop
metaKeystringWhich group the stack belonged to (strain, purity, batch)
modestringThe Sale money mode: 'auto', 'account' or 'item'
methodstringHow it was sold: 'street', 'corner', 'phone', 'drop' or 'bulk'

Your code runs safely: a mistake in it is logged, and the sale still completes.

shared/config.lua
Config.OnDrugSold = function(saleData)
    -- Example: pay a turf-control system on every street and corner sale
    if saleData.method ~= 'street' and saleData.method ~= 'corner' then return end
    local turfId = exports['op-crime']:getPlayerTurfZone(saleData.source)
    if turfId then
        TriggerEvent('op-crime:drugSold', saleData.source, turfId, saleData.finalPrice)
    end
end

Phone examined hook

Config.OnPhoneExamined runs on the server every time an officer opens a seized phone. src is the officer's server ID, and report holds the phone's number, contacts, meets and texts, plus report.text, a plain-text copy (see Evidence Bridge → The report).

shared/config.lua
Config.OnPhoneExamined = function(src, report)
end

shared/config.lua is also sent to every player's game. Don't put passwords or webhook URLs in it. To send reports somewhere, use the server event from your own resource instead (see Evidence Bridge → Layer 2: Hooks).

If you'd rather not edit this file, both hooks are also server events. See Exports → Server Events.

The Config Files

FileWhat's in itMenu location
shared/config.luaDebug, sale money, model swaps, and the two hooksCore → General, Advanced → Compatibility
shared/config/selling.luaStreet selling, heat, witnesses, time of dayCore → Street, Core → Heat & Witnesses
shared/config/corners.luaCorners and corner buyersCore → Corners
shared/config/phone.luaThe phone, clients, orders, meets, dead drops, reputation, no-signal areasPhone
shared/config/pressure.luaEvidence, evidence bridges, bulk buyers, stingsPressure
shared/config/drugs.luaSellable drugs and their pricingFile only

General

Menu: Core → General

SettingMenu labelTypeDefaultWhat it does
Config.DebugDebug logging (Advanced)booleanfalseWrites extra detail to the server log. Leave off on a live server.
Config.DirtyMoney.modeSale money → Paid asstring'auto'Where sale money goes, and where robberies take it from. 'auto' (Framework dirty money): marked bills on QBCore, the black_money item on QBX, the black_money account on ESX. 'account' (An account): the account below. 'item' (An item ($1 each)): the item below.
Config.DirtyMoney.accountSale money → Accountstring'black_money'The account used when Paid as is An account.
Config.DirtyMoney.itemSale money → Itemstring'black_money'The item used when Paid as is An item. Each one is worth $1. It must exist in your inventory.
shared/config.lua
Config.Debug = false

Config.DirtyMoney = {
    mode = 'auto',          -- 'auto' | 'account' | 'item'
    account = 'black_money',
    item = 'black_money',
}

Street

Menu: Core → Street → Street selling. These settings also apply to corner buyers, unless the Corners section says otherwise.

General

SettingMenu labelTypeDefaultWhat it does
Config.Selling.enabledEnabledbooleantrueTurns the Offer Drugs option on pedestrians on or off.
Config.Selling.targetIcon(not in menu)string'fas fa-hand-holding-dollar'The Font Awesome icon on the Offer Drugs option.
Config.Selling.targetLabel(not in menu)string'sell_offer_drugs'The translation key for the option's text. The words are in locales/en.json.
Config.Selling.targetDistanceReach (m)number5.0How close a player must be to a pedestrian to see Offer Drugs. 1 to 10.
Config.Selling.panelTitlePanel titlestring'Street Sale'The title at the top of the sell menu for street sales.
Config.Selling.cooldownCooldown between sales (ms)number5000How long after a sale before the player can offer to someone else. 5000 = 5 seconds.
Config.Selling.npcCooldownSame-buyer cooldown (ms)number120000How long before anyone can sell to the same pedestrian again. 120000 = 2 minutes. Kept by the server.
Config.Selling.policeJobsPolice jobstable{ 'police', 'sheriff' }The jobs that receive every police alert and count toward Police needed online. Must match your job names exactly.
Config.Selling.minPoliceOnlinePolice needed onlinenumber0How many players in those jobs must be online before anyone can sell: on the street, on a corner, at a phone meet or by leaving a dead drop. 0 = no requirement.
Config.Selling.onDutyOnlyOnly on-duty policebooleantrueOnly on-duty officers count toward the minimum, can read seized phones and can use the sting phone. With most dispatch resources, alerts are passed on through an on-duty officer's game. Without a dispatch resource, alerts go to every player with a police job, on duty or not. Turn it off if your framework has no duty system, or no officer will ever count.

Pricing

SettingMenu labelTypeDefaultWhat it does
Config.Selling.pricing.autoAcceptMaxAccepted at or belownumber0.90An offer at or below this share of the fair price is accepted. 0.90 = 90%.
Config.Selling.pricing.negotiateMaxNegotiable up tonumber1.25An offer up to this share can still be accepted or countered. Above it, it's refused. Can't be lower than Accepted at or below.
Config.Selling.pricing.negotiateAcceptChanceAccepted outright in that bandnumber0.5The chance an offer between the two limits is accepted instead of countered. 0.5 = 50%.
Config.Selling.pricing.counterOfferCounter-offernumber0.95The buyer's counter-offer, as a share of the fair price.
Config.Selling.pricing.counterExpiryCounter stays open (s)number60Seconds the player has to accept a counter-offer.

The sell menu's Their reaction meter uses these same limits, so what it shows always matches what the server does.

Risks

SettingMenu labelTypeDefaultWhat it does
Config.Selling.risks.policeCallChancePolice alertnumber0.15The chance each street or corner offer alerts police. 0.15 = 15%. Heat adds to it.
Config.Selling.risks.rejectChanceRefusalnumber0.15The chance a buyer refuses before price is considered. Time of day, gang territory and heat change it.
Config.Selling.risks.robberyChanceRobbery (gang NPCs)number0.15The chance a gang-model buyer robs the player instead. Heat adds to it.
Config.Selling.risks.robberyItemLossUnits takennumber3The most units a street or corner robber takes.
Config.Selling.risks.robberyCashPercentShare of dirty money takennumber0.25The share of the player's dirty money a robber takes. Also used when a phone client robs a player at a meet.
Config.Selling.risks.robberyWeaponRobber weaponstring'WEAPON_PISTOL'The weapon a street, corner or phone-client robber pulls.

Police alert

These settings are used for every alert from a street sale, corner sale or phone meet.

SettingMenu labelTypeDefaultWhat it does
Config.Selling.dispatch.codeCodestring'10-31'The code shown on the alert.
Config.Selling.dispatch.messageMessage locale keystring'sell_dispatch_message'The translation key for the alert text ("Suspicious activity reported").
Config.Selling.dispatch.blipData.spriteBlip → Spritenumber469The map blip icon.
Config.Selling.dispatch.blipData.colorBlip → Colournumber1The map blip colour (1 = red).
Config.Selling.dispatch.blipData.scaleBlip → Scalenumber0.8The map blip size.
Config.Selling.dispatch.timeBlip time (ms)number60000How long the alert stays. 60000 = 1 minute.

Buyers, camera and animations

SettingMenu labelTypeDefaultWhat it does
Config.Selling.blacklistedModelsNever sell totablecops, sheriffs, rangers, marines, paramedics, securityPedestrian models that never get Offer Drugs.
Config.Selling.gangModelsGang modelstable16 gang membersPedestrian models that can rob the player, and show the Dangerous warning. Used on corners too.
Config.Selling.camera.interpTimeCamera → Blend (ms)number500How long the camera takes to move in on the buyer.
Config.Selling.camera.distanceCamera → Distancenumber1.8How far in front of the buyer the camera sits.
Config.Selling.camera.heightOffsetCamera → Heightnumber0.3How high the camera sits.
Config.Selling.camera.fovCamera → Field of viewnumber50.0The camera's zoom. Lower is closer.
Config.Selling.anims(not in menu)tablesee belowThe buyer's thinking, accept and refuse animations.

Model lists use plain model names in quotes:

shared/config/selling.lua
blacklistedModels = {
    's_m_y_cop_01', 's_f_y_cop_01', 's_m_y_sheriff_01',
    's_m_y_ranger_01', 's_m_y_marine_01', 's_m_y_marine_02',
    's_m_y_marine_03', 's_m_m_paramedic_01', 's_m_m_security_01',
    'csb_cop',
},

gangModels = {
    'g_m_y_famca_01', 'g_m_y_famdnf_01', 'g_m_y_famfor_01',
    'g_m_y_ballaorig_01', 'g_m_y_ballaeast_01', 'g_m_y_ballasout_01',
    'g_m_y_lost_01', 'g_m_y_lost_02', 'g_m_y_lost_03',
    'g_m_y_mexgang_01', 'g_m_y_mexgoon_01', 'g_m_y_mexgoon_02',
    'g_m_y_salvaboss_01', 'g_m_y_salvagoon_01',
    'g_m_y_korean_01', 'g_m_y_korean_02',
},

anims = {
    npcThinking = { dict = 'amb@world_human_smoking@male@male_a@enter', clip = 'enter', duration = 2000 },
    npcAccept   = { dict = 'mp_common', clip = 'givetake1_a', duration = 1800 },
    npcReject   = { dict = 'gestures@m@standing@casual', clip = 'gesture_no_way', duration = 1000 },
},

Heat And Witnesses

Menu: Core → Heat & Witnesses. See Features → Heat for how heat plays out in game.

Heat

SettingMenu labelTypeDefaultWhat it does
Config.Heat.enabledEnabledbooleantrueTurns heat on or off. With heat off nothing gains heat, so once old heat has cooled off, police stings can't find a dealer.
Config.Heat.cellSizeArea size (m)number250.0The map is split into squares this wide. Each square has its own area heat.
Config.Heat.maxMaximum heatnumber100The most heat anything can have. The effects below are reached at this value.
Config.Heat.gain.saleHeat gained → Per salenumber2.0Heat every sale adds.
Config.Heat.gain.perUnitHeat gained → Per unitnumber0.25Extra heat per unit sold.
Config.Heat.gain.witnessHeat gained → Witness calls it innumber4.0Heat each snitching witness adds.
Config.Heat.gain.alertHeat gained → Police alertnumber8.0Heat each police alert adds to the area. A completed sting adds double.
Config.Heat.gain.cornerHeat gained → Corner multipliernumber1.5Corner sales multiply their heat by this.
Config.Heat.gain.robberyHeat gained → Robberynumber5.0Heat a robbery adds to the area.
Config.Heat.decay.areaLost per minute → Areanumber1.5Area heat lost per real minute.
Config.Heat.decay.cornerLost per minute → Cornernumber2.0Corner heat lost per real minute.
Config.Heat.decay.playerLost per minute → Dealernumber1.0A character's own heat lost per real minute.
Config.Heat.decay.numberLost per minute → Phone numbernumber0.75A phone number's heat lost per real minute.
Config.Heat.effects.refusalAt maximum heat → Extra refusal chancenumber0.25Extra refusal chance at maximum heat. Less heat, less extra, in a straight line.
Config.Heat.effects.alertAt maximum heat → Extra alert chancenumber0.30Extra police alert chance at maximum heat.
Config.Heat.effects.robberyAt maximum heat → Extra robbery chancenumber0.10Extra robbery chance at maximum heat.
shared/config/selling.lua
Config.Heat = {
    enabled  = true,
    cellSize = 250.0,   -- metres per map area
    max      = 100,
    gain    = { sale = 2.0, perUnit = 0.25, witness = 4.0, alert = 8.0, corner = 1.5, robbery = 5.0 },
    decay   = { area = 1.5, corner = 2.0, player = 1.0, number = 0.75 },   -- per real minute
    effects = { refusal = 0.25, alert = 0.30, robbery = 0.10 },           -- at max heat
}

Witnesses

SettingMenu labelTypeDefaultWhat it does
Config.Witnesses.enabledEnabledbooleantrueTurns witness checks on or off.
Config.Witnesses.radiusRadius (m)number15.0How far around a sale people count as witnesses.
Config.Witnesses.crowdLimitToo crowded atnumber4With this many people nearby, the buyer won't deal.
Config.Witnesses.snitchChanceSnitch chance per witnessnumber0.05Each witness's chance to call the police. 0.05 = 5%.
shared/config/selling.lua
Config.Witnesses = {
    enabled      = true,
    radius       = 15.0,
    crowdLimit   = 4,
    snitchChance = 0.05,
}

Time of day

Menu: Core → Heat & Witnesses → Time of day (Advanced). A list of in-game hour ranges, checked from the top; the first row that covers the hour is used. A range can wrap past midnight (21 to 4). Hours not covered use 1.0 for both.

FieldMenu labelTypeWhat it does
labelLabelstringA name for the row
fromFrom hournumberFirst hour, 0 to 23
toTo hournumberThe hour it ends. Can't equal From hour
acceptAcceptnumberHigher makes buyers more willing (the refusal chance is divided by it). 0.1 to 3
demandDemandnumberHigher brings corner buyers faster and makes phone clients text more often. 0 to 5
shared/config/selling.lua
Config.TimeOfDay = {
    { label = 'Night',     from = 21, to = 4,  accept = 1.2, demand = 1.3 },
    { label = 'Morning',   from = 4,  to = 10, accept = 0.7, demand = 0.6 },
    { label = 'Afternoon', from = 14, to = 18, accept = 1.0, demand = 1.1 },
}

Corners

Menu: Core → Corners, or run /drugselling corners. Players start a corner wherever they stand; there is nothing to place.

SettingMenu labelTypeDefaultWhat it does
Config.Corners.enabledEnabledbooleantrueTurns corners on or off.
Config.Corners.commandCommandstring'corner'The command players type to start and stop a corner, without the slash. A rename takes effect after a restart.
Config.Corners.radialRadial menu itembooleantrueAdds Work corner / Stop corner to the radial menu.
Config.Corners.radiusRadius (m)number15.0How far from where they started the player may move. The corner closes once they're 5 m past it.
Config.Corners.spacingSpacing (m)number40.0Two players can't work corners closer than this. 0 allows any distance.
Config.Corners.idleTimeoutCloses after idle (s)number180A corner closes for a player who goes this long without dealing with a buyer.
Config.Corners.buyerMinShortest wait (s)number25Fewest seconds between buyers. Can't be more than Longest wait.
Config.Corners.buyerMaxLongest wait (s)number70Most seconds between buyers.
Config.Corners.modeBuyersstring'both''walk' (walk up), 'drive' (drive up) or 'both'.
Config.Corners.substituteAcceptAccept rate for a substitutenumber0.6When the player offers a different drug than the buyer asked for, the buyer accepts only this share as often.
Config.Corners.anyDrugChanceBuyer asks for any drugnumber0.15Most buyers ask for a drug the player carries. This share ask for anything on the demand list, even a drug the player doesn't have.
Config.Corners.demandDemandtableweed, meth, cocaine, pillsWhat buyers ask for. See below.
Config.Corners.walkModelsWalk-up buyerstable10 street modelsModels for buyers who walk up when no local is nearby.
Config.Corners.driveModelsDriverstable4 street modelsModels for buyers who drive up.
Config.Corners.vehiclesBuyer vehiclestableemperor, primo, manana, tornado, buccaneer, voodoo, glendale, washingtonCars that drive-up buyers use. Phone clients who drive to a meet use them too.

Each Demand row:

FieldMenu labelTypeWhat it does
drugDrug typestringA drug key from Config.Drugs: weed, meth, cocaine or pills with the shipped config
weightHow oftennumberHow often buyers ask for this drug compared to the other rows. 0 means never
pricePrice multipliernumberWhat corner buyers pay for this drug against its fair price. 1 = normal, 1.5 = 50% more
shared/config/corners.lua
demand = {
    { drug = 'weed',    weight = 3, price = 1.0 },
    { drug = 'meth',    weight = 2, price = 1.1 },
    { drug = 'cocaine', weight = 2, price = 1.15 },
    { drug = 'pills',   weight = 2, price = 1.05 },
},

Phone

Menu: Phone → Phone → Trap phone.

SettingMenu labelTypeDefaultWhat it does
Config.Phone.enabledEnabledbooleantrueTurns trap phones on or off. Off: using a phone does nothing and no clients text.
Config.Phone.itemPhone itemstring'trap_phone'The phone item.
Config.Phone.simItemSIM card itemstring'trap_sim'The item that gives a phone a new number.
Config.Phone.chargerItemCharger itemstring'trap_charger'The battery pack item. Empty means phones only charge in vehicles.
Config.Phone.numberFormatNumber formatstring'555-####'How numbers look. Every # becomes a random digit. 555-#### allows 10,000 numbers; add more # for a bigger server.
Config.Phone.pinLengthPIN lengthnumber4How many digits a PIN has. 3 to 8.
Config.Phone.starterClientsStarter clientsnumber3Clients a phone gets the first time it's used. 0 to 10.
Config.Phone.firstOrderDelayFirst order after (s)number20The first starter client (who answers at any hour) texts an order this many seconds after a new phone is first used. -1 waits for the normal order checks.
Config.Phone.battery.standbyPerMinuteBattery → Standby drain (%/min)number0.35Battery lost per real minute while switched on.
Config.Phone.battery.perTextBattery → Per text sent (%)number0.5Battery each sent text or reply uses.
Config.Phone.battery.vehiclePerMinuteBattery → Vehicle charge (%/min)number4.0Battery gained per real minute while sitting in any vehicle. 0 turns vehicle charging off.
Config.Phone.battery.chargerAmountBattery → Charger adds (%)number60Battery one battery pack adds.
Config.Phone.battery.chargerTimeBattery → Charger time (ms)number8000How long using a battery pack takes.
Config.Phone.ringDistanceRing heard within (m)number12.0Everyone this close hears a phone ring when it isn't on silent. 0 turns the ring sound off; the holder still sees the text slide up.
Config.Phone.indoorsNoSignalNo signal indoorsbooleanfalseMakes every interior a no-signal area.
Config.Phone.propPhone propstring'prop_amb_phone'The phone model held in the player's hand.
Config.Phone.anim.dictTexting animation → Dictionarystring'cellphone@'The texting animation.
Config.Phone.anim.clipTexting animation → Clipstring'cellphone_text_read_base'The texting animation clip.
shared/config/phone.lua
Config.Phone = {
    enabled = true,
    item        = 'trap_phone',
    simItem     = 'trap_sim',
    chargerItem = 'trap_charger',
    numberFormat = '555-####',   -- every # is a random digit
    pinLength    = 4,
    starterClients  = 3,
    firstOrderDelay = 20,
    battery = {
        standbyPerMinute = 0.35,  -- % per real minute while on
        perText          = 0.5,
        vehiclePerMinute = 4.0,
        chargerAmount    = 60,
        chargerTime      = 8000,  -- ms
    },
    ringDistance = 12.0,
    indoorsNoSignal = false,
    prop = 'prop_amb_phone',
    anim = { dict = 'cellphone@', clip = 'cellphone_text_read_base' },
}

No-signal areas

Menu: Phone → No-signal areas (Advanced).

SettingMenu labelTypeDefaultWhat it does
Config.DeadZonesNo-signal areastable{}Areas with no signal. Drawn in the world with Draw new zone. A phone inside one can't send, and incoming texts wait until it's back in signal.

Each area has a Name, its corner points, and a bottom and top height.

Clients

Menu: Phone → Clients. See Features → Clients.

SettingMenu labelTypeDefaultWhat it does
Config.Clients.maxBaseBook size at the lowest tiernumber5How many clients a phone holds at the lowest reputation tier. Tiers add more.
Config.Clients.personasPersonastable5 personasThe kinds of client. See below. At least one is required.
Config.Clients.availabilityAvailability windowstable4 windowsThe hours a client answers. Each new client gets one at random. The same hour in both fields means any time.
Config.Clients.trust.startTrust changes → New client starts atnumber40A new client's trust.
Config.Clients.trust.dealTrust changes → Completed dealnumber5Trust for every completed deal.
Config.Clients.trust.onTimeTrust changes → On timenumber3Extra trust for being on time.
Config.Clients.trust.lateTrust changes → Latenumber-6Trust for being late. Doubled for very late. Also used when you cancel a meet or take your own drop back.
Config.Clients.trust.noShowTrust changes → No-shownumber-18Trust for not showing up.
Config.Clients.trust.goodBatchTrust changes → Good batchnumber4Trust for good quality.
Config.Clients.trust.badBatchTrust changes → Bad batchnumber-8Trust for bad quality.
Config.Clients.trust.stolenTrust changes → Drop stolennumber-12Trust when their dead drop was taken by someone else.
Config.Clients.trust.leaveBelowTrust changes → Stops answering at or belownumber5A client at or below this trust leaves the book.
Config.Clients.quality.goodQuality memory → Good batch abovenumber1.15A batch whose fair price is at least this share of its base price counts as good.
Config.Clients.quality.badQuality memory → Bad batch belownumber0.8A batch at or below this share counts as bad.
Config.Clients.acquire.streetChanceNew clients → Street buyer asks for your numbernumber0.15Chance after a completed street sale.
Config.Clients.acquire.cornerChanceNew clients → Corner buyer asks for your numbernumber0.20Chance after a completed corner sale.
Config.Clients.acquire.introChanceNew clients → Introduction after a dealnumber0.25Chance a trusted client introduces a friend after a deal.
Config.Clients.acquire.introMinTrustNew clients → Introduces at trustnumber70Trust a client needs to introduce someone.
Config.Clients.acquire.introMinDealsNew clients → After this many dealsnumber3Deals a client needs to introduce someone.
Config.Clients.acquire.homeRadiusNew clients → Lives within (m)number350.0A new client's home is within this distance of where they were met. Meets happen near their home.
Config.Clients.aliasesNamestable30 namesNames clients are given.
Config.Clients.modelsPed modelstable14 modelsModels phone clients spawn as. Clients met on the street keep the look they had.

Personas

FieldMenu labelWhat it does
keyKeyAn ID. Required
labelLabelShown on the Contacts screen
weightHow commonHow often new clients get this persona, compared to the others
budgetMin / budgetMaxLowest / Highest budgetTheir opening offer, as a share of the order's base value
countersCounters before walkingHow many times they haggle before giving up
toleranceGoes above their offer byHow far above their opening offer they'll go. 1.12 = 12%
qtyMin / qtyMaxSmallest / Largest orderHow many units they order
shared/config/phone.lua
personas = {
    { key = 'broke',   label = 'Broke',   weight = 20, budgetMin = 0.45, budgetMax = 0.75, counters = 1, tolerance = 1.05, qtyMin = 1, qtyMax = 2 },
    { key = 'cheap',   label = 'Cheap',   weight = 25, budgetMin = 0.70, budgetMax = 0.90, counters = 3, tolerance = 1.08, qtyMin = 1, qtyMax = 3 },
    { key = 'rushed',  label = 'Rushed',  weight = 15, budgetMin = 0.90, budgetMax = 1.10, counters = 1, tolerance = 1.10, qtyMin = 1, qtyMax = 3 },
    { key = 'regular', label = 'Regular', weight = 30, budgetMin = 0.90, budgetMax = 1.05, counters = 2, tolerance = 1.12, qtyMin = 1, qtyMax = 4 },
    { key = 'rich',    label = 'Rich',    weight = 10, budgetMin = 1.15, budgetMax = 1.60, counters = 2, tolerance = 1.20, qtyMin = 2, qtyMax = 6 },
},

availability = {
    { label = 'Evenings',    from = 17, to = 1 },
    { label = 'Late nights', from = 21, to = 5 },
    { label = 'Days',        from = 9,  to = 18 },
    { label = 'Any time',    from = 0,  to = 0 },
},

The broke, rushed and rich keys also have their own order texts in locales/en.json, used most of the time (6 in 10 orders). Other personas always use the general lines.

Orders And Meets

Menu: Phone → Orders & Meets.

Orders

SettingMenu labelTypeDefaultWhat it does
Config.Orders.tickSecondsCheck every (s)number30How often clients get a chance to text an order.
Config.Orders.chancePerTickChance per client per checknumber0.10Each available client's chance per check, before reputation and time of day.
Config.Orders.clientCooldownClient cooldown (s)number420Seconds after a client's last order ended before they order again. Half as long after Not now or Don't got it. Bulk buyers wait twice as long.
Config.Orders.maxOpenOpen orders per phonenumber4The most orders a phone can have going at once.
Config.Orders.replyTimeoutClient waits for a reply (s)number300Seconds a client waits for an answer, and again for a meet time, before giving up.
Config.Orders.menuWhat clients ask fortable9 rowsWhat clients can order. See below.

Each What clients ask for row:

FieldMenu labelWhat it does
keyKeyAn ID. Required
drugDrug typeThe drug key in Config.Drugs. Required
labelWhat clients call itThe word clients use in texts ("grams", "rock")
itemsItems that countAny of these items fills the order. The cheapest one's base price sets the client's opening offer

A row is only used when at least one of its items exists on your server and is listed in Config.Drugs.

shared/config/phone.lua
menu = {
    { key = 'weed_small',  drug = 'weed',    label = 'grams',   items = { 'weed_1g' } },
    { key = 'weed_eighth', drug = 'weed',    label = 'eighths', items = { 'weed_eighth' } },
    { key = 'meth',        drug = 'meth',    label = 'meth',    items = { 'meth_baggy', 'meth_baggy_glass', --[[ every meth baggy ]] } },
    { key = 'coke',        drug = 'cocaine', label = 'coke',    items = { 'cocaine_bag' } },
    { key = 'crack',       drug = 'cocaine', label = 'rock',    items = { 'crack_bag' } },
    { key = 'party',       drug = 'pills',   label = 'party pills', items = { 'party_pill', 'party_pill_bag', --[[ ... ]] } },
    { key = 'bars',        drug = 'pills',   label = 'bars',    items = { 'bar_pill', --[[ ... ]] } },
    { key = 'blues',       drug = 'pills',   label = 'blues',   items = { 'blue_pill', --[[ ... ]] } },
    { key = 'percs',       drug = 'pills',   label = 'percs',   items = { 'perc_pill', --[[ ... ]] } },
},

The full lists are in shared/config/phone.lua.

Meets

Times are in-game minutes. On a server with the standard GTA clock, one in-game minute passes in about two real seconds.

SettingMenu labelTypeDefaultWhat it does
Config.Meets.slotsTimes offerednumber3How many times the client offers when booking.
Config.Meets.leadMinutesEarliest slot aheadnumber120The first time offered is at least this far ahead.
Config.Meets.slotSpacingGap between slotsnumber120Gap between the times offered.
Config.Meets.earlyWindowClient waits earlynumber30The client is at the meet this long before the time, and the hand-off or drop can happen from then.
Config.Meets.lateGraceLate gracenumber60Up to this long after the time counts as late. After that it's very late. Can't be longer than No-show after.
Config.Meets.noShowAfterNo-show afternumber180After this long the client gives up: a no-show.
Config.Meets.spawnDistanceClient appears within (m)number150.0The client appears when the player carrying the phone comes this close.
Config.Meets.approachDistanceClient comes over within (m)number25.0Once the player is this close to the client (and inside the hand-over radius), a client on foot walks over and a client in a car pulls up to the kerb.
Config.Meets.meetRadiusHand-over radius (m)number30.0How close to the meet point the player must be to hand over.
Config.Meets.arriveByCarClient drives upnumber0.35Chance the client arrives by car.
Config.Meets.pointMinMeet point at least (m) from homenumber40.0The closest a hand-off point can be to the client's home.
Config.Meets.pointMaxMeet point at most (m) from homenumber220.0The farthest.
Config.Meets.robberyChanceRobbery at trust 0number0.25Chance a client robs the player at a meet, at trust 0.
Config.Meets.robberyTrustNo robbery from trustnumber35At this trust and above, clients never rob. In between, the chance falls in a straight line.
shared/config/phone.lua
Config.Meets = {
    slots = 3, leadMinutes = 120, slotSpacing = 120,           -- in-game minutes
    earlyWindow = 30, lateGrace = 60, noShowAfter = 180,
    spawnDistance = 150.0, approachDistance = 25.0, meetRadius = 30.0, arriveByCar = 0.35,
    pointMin = 40.0, pointMax = 220.0,
    robberyChance = 0.25, robberyTrust = 35,
}

Dead Drops

Menu: Phone → Dead Drops, or run /drugselling drops.

SettingMenu labelTypeDefaultWhat it does
Config.Drops.enabledEnabledbooleantrueTurns dead drops on or off. Off: clients only offer hand-offs.
Config.Drops.spotsDrop spotstable10 spots in south Los SantosWhere drops happen, each with a Name and a Spot (use Place in world). A drop always uses the spot nearest the client's home, however far. With no spots, clients only offer hand-offs.
Config.Drops.pickupDelayClient collects after (s)number60Real seconds after the package is left before the client collects it.
Config.Drops.payoutPaymentstring'pickup''pickup' (Left at the drop): cash is left where the bag was, for the player to collect. 'instant' (Paid on collection): paid straight to the player if they're online with the phone, otherwise left at the drop.
Config.Drops.cashWindowCash waits (s)number600Real seconds the cash waits at the drop before it's gone.
Config.Drops.visibleRangeVisible within (m)number60.0How close players must be to see bags and cash at drops.
Config.Drops.markerRangeMarker shown within (m)number20.0How close the player with the drop must be to see the marker over the spot, their package and their cash. 0 hides the markers.
Config.Drops.propPackage propstring'prop_food_bag2'The bag model.
Config.Drops.cashPropCash propstring'prop_cash_envelope_01'The cash model.
shared/config/phone.lua
Config.Drops = {
    enabled = true,
    spots = {
        { coords = vector3(124.97, -1681.01, 28.45) },
        -- ...9 more in south Los Santos
    },
    pickupDelay  = 60,        -- real seconds
    payout       = 'pickup',  -- 'pickup' | 'instant'
    cashWindow   = 600,       -- real seconds
    visibleRange = 60.0,
    markerRange  = 20.0,
    prop         = 'prop_food_bag2',
    cashProp     = 'prop_cash_envelope_01',
}

Reputation

Menu: Phone → Reputation → Reputation tiers. Every change in a client's trust moves the phone's reputation by the same amount. Tiers must be listed from lowest to highest.

FieldMenu labelWhat it does
labelLabelThe tier's name, shown on the phone
repFrom reputationReputation needed
orderRateOrder rateMultiplies how often clients text
clientsExtra clientsBook slots added on top of Book size at the lowest tier
tolerancePrice toleranceMultiplies how far clients go above their offer
bulkBulk buyers dealBulk buyers deal with phones at this tier
shared/config/phone.lua
Config.Reputation = {
    tiers = {
        { rep = 0,   label = 'Nobody',     orderRate = 1.0,  clients = 0,  tolerance = 1.00, bulk = false },
        { rep = 60,  label = 'Corner Boy', orderRate = 1.0,  clients = 2,  tolerance = 1.03, bulk = false },
        { rep = 180, label = 'Hustler',    orderRate = 1.15, clients = 4,  tolerance = 1.06, bulk = false },
        { rep = 400, label = 'Plug',       orderRate = 1.3,  clients = 7,  tolerance = 1.10, bulk = true },
        { rep = 800, label = 'Kingpin',    orderRate = 1.5,  clients = 10, tolerance = 1.15, bulk = true },
    },
}

Evidence

Menu: Pressure → Evidence → Seized phones. See Features → Police: Seized Phones.

SettingMenu labelTypeDefaultWhat it does
Config.Evidence.enabledEnabledbooleantrueTurns Evidence View on or off. Off: officers use seized phones like anyone else (a PIN still locks them out).
Config.Evidence.jobsJobstable{ 'police', 'sheriff' }Jobs that get Evidence View on a phone that isn't theirs (on duty, if Only on-duty police is on).
Config.Evidence.minGradeMinimum gradenumber0The lowest job grade that gets Evidence View.
Config.Evidence.unlock.modeGetting in → Modestring'timed''none' (Opens straight away), 'pin' (Needs the PIN, if the phone has one) or 'timed' (Timed extraction).
Config.Evidence.unlock.durationGetting in → Extraction time (ms)number15000How long a timed extraction takes. 0 opens straight away.
Config.Evidence.unlock.itemGetting in → Tool neededstring''An item the officer must carry for a timed extraction. Empty: no tool needed.
Config.Evidence.trace.enabledNumber trace → EnabledbooleantrueTurns number traces on or off.
Config.Evidence.trace.radiusNumber trace → Blurred to (m)number150.0The radius of the circle a trace shows. The phone is somewhere inside it.
Config.Evidence.trace.durationNumber trace → Blip stays (s)number60How long the trace stays on the map.
Config.Evidence.trace.cooldownNumber trace → Cooldown (s)number120Seconds between traces, per officer.
Config.Evidence.trace.showOwnerNumber trace → Names the registered ownerbooleanfalseShows the phone's registered owner (its first user) on traces, in Evidence View and in reports. Off keeps burners anonymous.
shared/config/pressure.lua
Config.Evidence = {
    enabled  = true,
    jobs     = { 'police', 'sheriff' },
    minGrade = 0,
    unlock = { mode = 'timed', duration = 15000, item = '' },   -- 'none' | 'pin' | 'timed'
    trace = { enabled = true, radius = 150.0, duration = 60, cooldown = 120, showOwner = false },
}

Evidence bridges

Menu: Pressure → Evidence → Evidence bridges (Advanced).

SettingMenu labelTypeDefaultWhat it does
Config.Integrations.evidenceBridgestable{ { name = 'oxide-police', enabled = true } }The MDTs that can receive seized-phone reports, tried in order. The first one that's switched on, has a bridge file, and whose resource is running gets the report. Resource must be the MDT's resource name.
shared/config/pressure.lua
Config.Integrations = {
    evidence = {
        { name = 'oxide-police', enabled = true },
    },
}

To connect another MDT, see Evidence Bridge.

Bulk Buyers

Menu: Pressure → Bulk Buyers. See Features → Bulk Buyers.

SettingMenu labelTypeDefaultWhat it does
Config.Bulk.enabledEnabledbooleantrueTurns bulk buyers on or off.
Config.Bulk.chancePerTickReaches out, per order checknumber0.02Chance per order check that a new bulk buyer texts a phone at a bulk tier. Also each bulk contact's chance to order again.
Config.Bulk.maxBuyersBulk contacts per phonenumber2The most bulk contacts a phone can have.
Config.Bulk.discountPays this share of fairnumber0.8Bulk buyers open at this share of the order's base value, and go at most 5% higher.
Config.Bulk.ambushChanceAmbush at standing 0number0.12Chance the bodyguards attack, at trust 0. Falls to nothing at trust 100.
Config.Bulk.bodyguardsBodyguardsnumber2Bodyguards who come to a bulk meet. 0 to 4. At a bulk meet, up to this many bodyguards standing within 15 m of the buyer aren't counted as witnesses.
Config.Bulk.weaponBodyguard weaponstring'WEAPON_PISTOL'The weapon the bulk buyer and their bodyguards pull in an ambush.
Config.Bulk.modelsPed modelstable4 gang modelsModels bulk buyers and their bodyguards use. Only peds with these models are left out of the witness count.
Config.Bulk.vehiclesVehiclestableballer, cavalcade, grangerCars bulk buyers arrive in.
Config.Bulk.namesNamestable5 namesNames bulk buyers are given.
Config.Bulk.menuWhat they buytable3 rowsLike What clients ask for, plus Smallest order (qtyMin) and Largest order (qtyMax).
shared/config/pressure.lua
menu = {
    { key = 'weed_bulk', drug = 'weed',    label = 'ounces', items = { 'weed_ounce', 'weed_brick' }, qtyMin = 3,  qtyMax = 8 },
    { key = 'meth_bulk', drug = 'meth',    label = 'meth',   items = { 'meth_baggy', 'meth_baggy_glass', 'meth_baggy_blue_sky', 'meth_baggy_ice' }, qtyMin = 20, qtyMax = 50 },
    { key = 'coke_bulk', drug = 'cocaine', label = 'bricks', items = { 'cocaine_brick' }, qtyMin = 1, qtyMax = 3 },
},

Stings

Menu: Pressure → Stings → Police stings. See Features → Police: Stings And The Heat Map.

SettingMenu labelTypeDefaultWhat it does
Config.Stings.enabledEnabledbooleantrueTurns the sting phone on or off.
Config.Stings.itemSting phone itemstring'sting_phone'The sting phone item.
Config.Stings.jobsJobstable{ 'police', 'sheriff' }Jobs that can use the sting phone (on duty, if Only on-duty police is on). Players who can use it are never picked as the target dealer.
Config.Stings.cooldownServer-wide cooldown (s)number1800Seconds between stings, for the whole server. 1800 = 30 minutes.
Config.Stings.minHeatOnly dealers with heat fromnumber10Dealers with less heat than this are never picked.
Config.Stings.heatMap.enabledHeat map → EnabledbooleantrueShows the Heat map option on the sting phone.
Config.Stings.heatMap.thresholdHeat map → Shows areas from heatnumber30Areas with at least this much heat show up.
Config.Stings.heatMap.delayHeat map → Delay (s)number300The map shows heat as it was this many seconds ago.
Config.Stings.heatMap.durationHeat map → Blips stay (s)number60How long the heat map stays. Each officer can open it once every 30 seconds.
Config.Stings.heatMap.blip.colorHeat map → Blip → Colournumber1The circles' colour.
Config.Stings.heatMap.blip.alphaHeat map → Blip → Opacitynumber110How see-through the circles are. 10 to 255.
Config.Stings.alert.codeHand-over alert → Codestring'10-66'The code on the alert when a dealer hands over to an undercover buyer.
Config.Stings.alert.messageHand-over alert → Message locale keystring'sting_alert_message'The translation key for the alert text ("Undercover buy completed").
Config.Stings.alert.blipDataHand-over alert → Bliptablesprite 161, colour 1, scale 1.0The alert's map blip.
Config.Stings.alert.timeHand-over alert → Blip time (ms)number120000How long the alert stays.
shared/config/pressure.lua
Config.Stings = {
    enabled  = true,
    item     = 'sting_phone',
    jobs     = { 'police', 'sheriff' },
    cooldown = 1800,   -- seconds, server-wide
    minHeat  = 10,
    heatMap = { enabled = true, threshold = 30, delay = 300, duration = 60, blip = { color = 1, alpha = 110 } },
    alert = {
        code     = '10-66',
        message  = 'sting_alert_message',
        blipData = { sprite = 161, color = 1, scale = 1.0 },
        time     = 120000,
    },
}

Compatibility

Menu: Advanced → Compatibility → Model swaps (Advanced).

SettingMenu labelTypeDefaultWhat it does
Config.ModelSwapsModel swapstable{}Replaces a model this resource uses with one your game build has. Each row has Shipped model (from) and Your model (to).

If a player's game can't load a model (a buyer, a car, a prop), online admins get a notification naming the model, pointing at /drugselling settings. Add a swap here to fix it.

shared/config.lua
Config.ModelSwaps = {
    -- { from = 'a_m_y_stbla_01', to = 'a_m_y_stbla_02' },
}

Drugs (Config File Only)

shared/config/drugs.lua defines every sellable drug. It's never stored in the database: edit the file, save it, and restart the server. The shipped file has weed, meth, cocaine and pills.

Drug shape

shared/config/drugs.lua
Config.Drugs.drugkey = {
    label = 'Display Name',
    items = { ... },             -- item name -> { label, basePrice, xpUnits }
    metaKeyFormat = '{field1}:{field2}',
    groupLabel = 'metadata_field',
    gradeDisplay = { ... },
    progression = { ... },       -- optional
    requiredLevel = 0,
    modifiers = { ... },
}

The key (drugkey) is what Drug type fields in the menu refer to (corner demand, order rows).

Items

items maps item names to a label and a base price. Only items listed here can be sold.

items = {
    weed_1g     = { label = 'Weed (1g)',     basePrice = 25, xpUnits = 1 },
    weed_eighth = { label = 'Weed (1/8 oz)', basePrice = 75, xpUnits = 3 },
},
FieldWhat it does
labelThe name shown in the sell menu
basePriceThe price for one, before modifiers
xpUnitsOptional. How many progression units one of this item counts as when it's sold, so bigger packages can pay more XP. Leave it out and each item counts as 1. It must be a number above 0; anything else counts as 1 and the server log shows one warning for that item

A sale pays xpPerUnit × quantity × xpUnits XP, rounded to a whole number (see Progression). The sold count sent to the drug's resource is always the number of items, whatever xpUnits says.

The shipped weed items set xpUnits to their weight in grams. Meth, cocaine and pills don't set it, so each item counts as 1.

The default 10 XP per gram is intentional: a 35 g brick pays 350 XP, exactly as 35 single grams do. Packaging does not reduce XP.

Weed itemxpUnitsXP per item at the default xpPerUnit (10)
weed_1g110
weed_eighth330
weed_quarter660
weed_half12120
weed_ounce24240
weed_brick35350

A brick is pressed from 10 dried buds, and each bud trims to 2–5 grams, so 35 is about what a brick holds on average.

Grouping

metaKeyFormat keeps different qualities apart in the sell menu, so they're never sold as one stack. Each {field} is replaced with that value from the item's metadata.

DrugmetaKeyFormatgroupLabel
weed'{strain_id}:{quality}'strain_name
meth'{variant}:{purity}'variant_name
cocaine'{purity}'not set (uses the item label)
pills'{batch}:{bagged}:{packed}'brand

groupLabel is the metadata field shown as the group's name. Leave it out to use the item's own label.

Grade display

FieldWhat it does
gradeMetaThe metadata field holding the grade text (for example quality)
scoreMetaAn optional number field shown next to the grade (for example quality_score)
colorsA colour for each grade, as Tailwind CSS class names like text-purple-400

Progression

progression is optional. It links a drug to the resource that makes it.

FieldWhat it does
resourceThe resource to call
levelExportIts export that returns the player's level
addXpExportIts export that gives XP
addSoldExportIts export that counts units sold
xpPerUnitXP per progression unit sold. One item is one unit, unless the item sets xpUnits

requiredLevel is checked on every way of selling (street, corner, phone hand-offs and dead drops), but only while the resource is running. If it isn't, the sale goes through and XP is skipped.

DrugrequiredLevelxpPerUnit
weed210 per gram
meth015 per item
cocaine015 per item
pills03 per item

Weed XP follows weight: a gram pays 10 XP and an ounce pays 240, so a crop earns the same XP whether it's sold as single grams or packed into ounces and bricks. How fast a player levels depends on how much weed they sell, not how many sales they make. To slow or speed up weed levelling, change weed's xpPerUnit; to change how one package compares with another, change its xpUnits.

Modifiers

modifiers is an ordered list. Each one adds a multiplier or a bonus to the fair price:

fairPrice = basePrice * (all multipliers) * (1 + all bonuses)

Every modifier has a type and a category ('multiplier' or 'bonus').

grade_multiplier

{
    type = 'grade_multiplier',
    category = 'multiplier',
    field = 'purity',
    grades = { Pure = 1.4, High = 1.15, Mid = 1.0, Low = 0.75, Dirty = 0.5 },
}

score_multiplier

A 0–100 score on a curve between a lowest and highest multiplier. If the item has no score, the fallback is used.

{
    type = 'score_multiplier',
    category = 'multiplier',
    field = 'quality_score',
    minMultiplier = 0.4,
    maxMultiplier = 1.5,
    curve = 1.2,
    fallback = {
        type = 'grade_multiplier',
        field = 'quality',
        grades = { Gas = 1.4, High = 1.15, Mid = 1.0, Poor = 0.75, Dirt = 0.5 },
    },
}

variant_multiplier

{
    type = 'variant_multiplier',
    category = 'multiplier',
    field = 'variant',
    variants = { crystal = 1.0, glass = 1.3, blue_sky = 1.6, street_mix = 0.7 },
    display = { label = 'Variant', color = 'text-blue-400' },
}

trait_bonus

Looks up traits from another resource's export and adds a bonus for each trait at or above its threshold, up to maxBonus.

{
    type = 'trait_bonus',
    category = 'bonus',
    sourceField = 'strain_id',
    exportResource = 'oxide-weed',
    exportName = 'GetStrainData',
    traitsPath = 'traits',
    traits = {
        thc     = { threshold = 75, bonus = 0.10, label = 'THC' },
        potency = { threshold = 70, bonus = 0.08, label = 'POT' },
    },
    maxBonus = 0.35,
}

The shipped weed entry lists seven traits: thc, potency, flavor, frost, density, aroma and cbd.

metadata_bonus

A bonus for each step a number on the item is at or above startValue, up to maxBonus.

{
    type = 'metadata_bonus',
    category = 'bonus',
    field = 'generation',
    startValue = 2,
    bonusPerStep = 0.05,
    maxBonus = 0.25,
    display = { label = 'Crossbred', color = 'text-cyan-400' },
}

export_multiplier

Asks another resource for the multiplier. The export gets the item's metadata and who is buying where, so it can price each sale differently. If the resource isn't running, the multiplier is 1.0. This is how pills are priced by oxide-pills.

{
    type = 'export_multiplier',
    category = 'multiplier',
    exportResource = 'oxide-pills',
    exportName = 'PriceFor',
}

See Exports → PriceFor for what the export must return.

Adding your own drug

Add a new entry under Config.Drugs in shared/config/drugs.lua.
Use item names that already exist in your inventory.
Set metaKeyFormat to the metadata your items carry (use a fixed text like 'none' if they carry none).
Set gradeDisplay and the modifiers you want.
Save the file and restart the server.
To let corner buyers and phone clients ask for it, add a Demand row on your corners and a row to What clients ask for in the settings menu, using the new drug's key.
shared/config/drugs.lua
Config.Drugs.heroin = {
    label = 'Heroin',
    items = {
        heroin_baggy = { label = 'Heroin Baggy', basePrice = 150 },
        heroin_brick = { label = 'Heroin Brick', basePrice = 2200 },
    },
    metaKeyFormat = '{purity}',
    groupLabel = 'purity_label',
    gradeDisplay = {
        gradeMeta = 'purity',
        scoreMeta = nil,
        colors = { Pure = 'text-purple-400', Cut = 'text-yellow-400', Weak = 'text-red-400' },
    },
    requiredLevel = 0,
    modifiers = {
        {
            type = 'grade_multiplier',
            category = 'multiplier',
            field = 'purity',
            grades = { Pure = 1.5, Cut = 1.0, Weak = 0.6 },
        },
    },
}

Next Steps