qb-crypto
qb-crypto implements the in-game cryptocurrency qbit: a fluctuating price, a per-character
portfolio, wallet-to-wallet transfers, and a cryptostick item that can be exchanged for coins.
The cryptostick reward appears in several other resources, including
qb-garbagejob, qb-towjob, and
qb-houserobbery, which makes this resource the sink at the end of
those loops.
The manifest at the commit linked above declares version 1.2.1.
Dependencies and start order
The manifest declares dependency 'qb-minigames' and loads @qb-core/shared/locale.lua, locales
and config.lua as shared scripts, @oxmysql/lib/MySQL.lua and server.lua on the server, and
client.lua on the client.
The README adds qb-phone and states plainly that buying and selling do
not work without it. The buy, sell, and transfer paths all call qb-phone:client:AddTransaction,
and the trading UI itself is a phone app.
Start order: oxmysql, qb-core, qb-minigames, qb-phone, then qb-crypto.
Required shared item: cryptostick.
Database
No SQL file ships with the resource, but it reads and writes a crypto table with at least the
columns crypto, worth, and history, where history holds JSON. At this commit the code will
INSERT a row for the configured coin if the UPDATE affects no rows, so the table must exist but
the row does not have to.
It also reads and writes the players table directly when transferring coins to an offline wallet,
rewriting the money JSON column. That path bypasses the normal money API, so it produces no
transaction log entry in your core.
Configuration
config.lua defines two global tables, Crypto and Ticker. Note it does not use the usual
Config name.
Crypto
| Key | Default | Meaning |
|---|---|---|
Lower | 500 | Lower bound for the simulated price |
Upper | 5000 | Upper bound |
Worth['qbit'] | 1000 | Starting value, overwritten from the database at start |
History['qbit'] | {} | Price history, persisted as JSON |
Labels['qbit'] | 'Qbit' | Display name |
Exchange.coords | vector3(1276.21, -1709.88, 54.57) | Where a cryptostick is exchanged |
Coin | 'qbit' | The coin this resource manages |
RefreshTimer | 10 | Minutes between simulated price moves |
ChanceOfCrashOrLuck | 2 | Percent chance of a large move |
Crash | { 20, 80 } | Percent range of a crash |
Luck | { 20, 45 } | Percent range of a spike |
ChanceOfDown | 30 | Threshold for a small downward move |
ChanceOfUp | 60 | Threshold for a small upward move |
CasualDown | { 1, 10 } | Percent range of a small drop |
CasualUp | { 1, 10 } | Percent range of a small rise |
Ticker, optional
Setting Ticker.Enabled = true replaces the simulated price with a real market price pulled from
the CryptoCompare API.
| Key | Default | Meaning |
|---|---|---|
Enabled | false | Off by default |
coin | 'BTC' | Symbol to track, use the ticker symbol not the full name |
currency | 'USD' | Quote currency |
tick_time | 2 | Minutes between requests, the documented minimum |
Api_key | 'put_api_key_here' | Your CryptoCompare key |
Error_handle | table | Maps API error strings to readable messages |
The README notes the free tier allows 100,000 calls per month and 250,000 lifetime, and that a two minute interval lasts roughly a year against the lifetime cap.
Enabling this makes your server issue outbound HTTP requests to a third party on a timer. Confirm that is acceptable for your hosting before switching it on, and keep the API key out of any public repository.
Commands
| Command | Permission | Effect |
|---|---|---|
/setcryptoworth <crypto> <value> | admin | Sets the coin’s value, appends a history entry, broadcasts the change, and persists it |
/checkcryptoworth | user | Reports the current qbit value |
/crypto | user | Reports the caller’s qbit balance and its cash value |
All three are registered through QBCore.Commands.Add, so /setcryptoworth is gated by the
qbcore.admin ace. See Commands and key mappings.
/setcryptoworth calls math.ceil(tonumber(args[2])) without checking that tonumber succeeded, so
a non-numeric second argument raises a server-side Lua error rather than the intended notification.
Server surface
| Handler | Kind | Arguments |
|---|---|---|
qb-crypto:server:FetchWorth | server event | none |
qb-crypto:server:ExchangeFail | server event | none, consumes one cryptostick |
qb-crypto:server:TransferCrypto | callback | data with Coins and WalletId |
qb-crypto:client:UpdateCryptoWorth | client event | coin, worth, history |
Transfers
TransferCrypto is the most interesting handler. Before using the supplied wallet ID in a SQL
LIKE pattern it strips the characters %, $, and ; from both the wallet ID and the coin
amount, then builds the pattern and passes it as a bound parameter:
local query = '%"walletid":"' .. data.WalletId .. '"%'
local result = MySQL.query.await('SELECT * FROM `players` WHERE `metadata` LIKE ?', { query })The value is bound, not concatenated into the statement, so this is not string-built SQL. The
character stripping exists to stop % from being used as a wildcard inside the LIKE pattern
itself, which would otherwise let one lookup match many players.
The balance check is Player.PlayerData.money.crypto >= tonumber(data.Coins). A negative Coins
value satisfies that comparison. Clamp it before shipping:
-- patched into server.lua
local coins = math.floor(tonumber(data.Coins) or 0)
if coins <= 0 then return cb('notenough') endIf the recipient is offline, the resource decodes their players.money JSON, adds the coins, and
writes it back. If that character is online elsewhere, the write can be overwritten by the normal
save path. Prefer transferring to online players, or add your own offline handling.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Cannot buy or sell | qb-phone is not started. The README states this explicitly |
| Price never changes | RefreshTimer has not elapsed, or Ticker.Enabled is true and the API request is failing |
| Ticker errors in the console | Wrong coin symbol, wrong currency, or a missing API key. Ticker.Error_handle maps the common ones to readable text |
| Value resets after a restart | The crypto table row is missing, so the starting Crypto.Worth from the config is used |
Server error on /setcryptoworth | The second argument was not numeric |
| Exchange does nothing | The player holds no cryptostick, or the item is missing from your shared items |
| Offline transfer lost | The recipient’s players.money write was overwritten by their own save |
Proposed manual smoke test
Not yet executed. On staging, with a database backup:
- Confirm the
cryptotable exists and thatcryptostickis in your shared items. - Run
/checkcryptoworth, wait outRefreshTimer, and confirm the value moved. - Set a value with
/setcryptoworth, restart the server, and confirm it persisted. - Transfer coins between two online characters and confirm both balances.
- Repeat with the recipient offline and confirm the balance after they reconnect.
- Exchange a
cryptostickand confirm the coins arrive.