Crypto & Payments
Charge and pay players in Satchel's crypto, and take payments through the laptop's pay sheet.
Two ways for another resource to take money from players through the laptop. The crypto exports read Satchel's prices and wallets and charge or pay a character in its coins, for example a dark web seller that only takes Bastion. Payments put the laptop's pay sheet in front of the player from your app or site: your server decides the amount, the player confirms with their bank or their coins, and you deliver once it is paid.
General Information
Every export on this page is a server export.
The crypto exports only exist while
enabledistrueinshared/unlocked/crypto.lua. The payment exports needConfig.Sdk.payments.enabledinshared/unlocked/sdk.lua; with it off they answer"disabled",nil, an empty list orfalse. WhenConfig.Sdk.allowlistis a table, only the resources it lists can create payments.Exports marked Yields wait on the database. Call them from a thread, an event handler or a command, not while your script loads.
Types / Classes
CryptoPaymentOptionsandCryptoPaymentResultare intypes/crypto.lua, andSdkPaymentOptions,SdkPaymentandSdkPaymentStatusintypes/sdk.luainsidezyke_laptop.
#Crypto
Satchel's coins are BSN (Bastion), MRD (Meridian), QIL (Quill), TLY (Tally), FLT (Flint), MUTT (Mutt) and SHD (Shade), of which the app lists those set in coins in shared/unlocked/crypto.lua. Coin ids aren't case sensitive. See the Config for the coins, the rate, the networks and their fees.
A target is a player's server id or a character identifier. An amount is one of:
- A number, or
{fiat = 500}: in-game money. It needs a fresh price, so it fails with"stale"while the market is behind. {coins = 0.01}or{coins = "0.01"}: coins. It works on a stale price. More decimals than the coin has is refused, not rounded.
Each character has one wallet with an address per coin. It belongs to the character, not their laptop account, and is made the first time anything asks for it.
#Charges and Payouts
Charges and payouts are transfers on the coin's network like a player's own:
ChargeCryptotakes the coins and the network fee from the player at once. The transfer then confirms after the coin's blocks. The coins never come back, so a resource that is happy with an unconfirmed payment can hand over the goods right away.GiveCryptoputs a payout on its way. The player can spend it once it confirms, and the paying side covers the fee.
The result carries the transfer's transferId, its status ("pending") and confirmAt, a Unix time. The server event zyke_laptop:crypto:Confirmed fires with the transfer when it confirms, and zyke_laptop:crypto:Charged or zyke_laptop:crypto:Given fires straight after the export. See Server Local Events.
| Option | Default | Description |
|---|---|---|
instant | false | true settles at once with no network fee and no transfer, as a transfer inside one exchange does. The result has no transferId and its status is "confirmed". |
priority | The coin's usual one | "slow", "normal" or "fast". A priority the coin doesn't have falls back to its usual one, "normal" where it has it. |
Both show in the player's activity with the calling resource and the reason, cut to 128 characters.
#Charge Crypto
Yields. Takes crypto from a character.
coinIdis a listed coin or"any". With"any"the amount must be money, and it is taken in the coin with the most value that covers it and its network fee. Amounts of money are rounded up to whole base units, so the charge covers all of it.Returns
trueand aCryptoPaymentResult, orfalseand one of:
"invalid": the target, coin, amount or options aren't valid, or"any"was given a coin amount."stale": a money amount and no fresh price."belowMinimum": below the coin's dust limit (minAmount). Instant charges have none."insufficient": the wallet can't cover the amount and the fee."failed": the database refused it.Example:
lua---@param target integer | string @ Player server id or character identifier ---@param coinId string @ A listed coin's id or "any" ---@param amount number | {fiat?: number, coins?: number | string} ---@param reason? string @ Shown in the player's activity ---@param options? CryptoPaymentOptions ---@return boolean charged ---@return CryptoPaymentResult | string resultOrError local charged, result = exports["zyke_laptop"]:ChargeCrypto(source, "any", 500, "Onion Market order #1042") local charged, result = exports["zyke_laptop"]:ChargeCrypto(source, "BSN", {coins = 0.01}, "Onion Market order #1042", {priority = "fast"}) -- result is {coin, units, amount, fiat, feeUnits, fee, txId, transferId, status, confirmAt, confirmations} AddEventHandler("zyke_laptop:crypto:Confirmed", function(transfer) if (charged and transfer.id == result.transferId) then -- Deliver the order end end)
| Result field | Description |
|---|---|
coin | The coin it was taken in. |
units / amount | The amount in exact base units, and in coins. |
fiat | The money amount asked for, rounded to whole money. nil for a coin amount without a fresh price. |
feeUnits / fee | The network fee the player paid on top. 0 for payouts and instant charges. |
txId | The player's activity row. |
transferId | The network transfer. nil for an instant charge. |
status | "pending" until it confirms, "confirmed" for an instant charge. |
confirmAt | Unix time the transfer confirms. |
confirmations | Blocks the coin needs before the coins can be spent. |
#Give Crypto
Yields. Pays crypto to a character, such as a buyer paying for a stolen part. Amounts of money are rounded down to whole base units. The character gets a laptop notification when they are online.
Returns
trueand aCryptoPaymentResultwithtxIdempty until it confirms, orfalseand"invalid","stale","belowMinimum"or"failed"."failed"includes a character whose wallet couldn't be made.Example:
lua---@param target integer | string @ Player server id or character identifier ---@param coinId string @ A listed coin's id ---@param amount number | {fiat?: number, coins?: number | string} ---@param reason? string @ Shown in the player's activity ---@param options? CryptoPaymentOptions ---@return boolean given ---@return CryptoPaymentResult | string resultOrError local given, result = exports["zyke_laptop"]:GiveCrypto(source, "MRD", {fiat = 1200}, "Sold a part to the chopshop") local given, result = exports["zyke_laptop"]:GiveCrypto(source, "FLT", {fiat = 50}, "Shift bonus", {instant = true})
#Get Crypto Transfer
Yields. Returns a charge's, payout's or player's transfer by id, or
nil. Use it to check a payment after your resource restarted, when you missed itsConfirmedevent.statusis"pending"or"confirmed".kindis"send"for a player's transfer,"spend"for a charge and"payout"for a payout.Example:
lua---@param transferId integer ---@return table? transfer @ {id, kind, coin, status, priority, units, amount, feeUnits, confirmations, createdAt, includedAt, confirmAt, sender, recipient, resource, note} local transfer = exports["zyke_laptop"]:GetCryptoTransfer(transferId) if (transfer and transfer.status == "confirmed") then -- Deliver the order end
#Get Crypto Balance
Yields. Returns what a character holds of one coin, in coins and in exact base units. Coins still on their way in aren't counted. Returns
0, 0for a character without a wallet, an invalid target or a coin that isn't listed.Example:
lua---@param target integer | string ---@param coinId string ---@return number amount @ Coins ---@return integer units @ Base units, exact local amount, units = exports["zyke_laptop"]:GetCryptoBalance(source, "BSN")
#Get Crypto Wallet
Yields. Returns the character's addresses, every listed coin they hold more than nothing of, and what it is worth at the last prices, stale ones included.
totalis the sum of the values in money. Coins on their way in aren't counted. Makes the wallet when the character has none, and returnsnilfor an invalid target or when an address couldn't be made.Example:
lua---@param target integer | string ---@return table? wallet @ {addresses = {[coinId] = address}, total = integer, balances = {[coinId] = {units, amount, value}}} local wallet = exports["zyke_laptop"]:GetCryptoWallet(source)
#Get Crypto Address
Yields. Returns the character's address of a coin, making their wallet when they have none. Returns
nilfor an invalid target or a coin that isn't listed.Example:
lua---@param target integer | string ---@param coinId string ---@return string? address local address = exports["zyke_laptop"]:GetCryptoAddress(source, "BSN") -- "bsn1q..."
#Get Crypto Address Owner
Yields. Returns the character identifier an address belongs to and the coin it is for. The coin is read from the address format. Returns
nil, nilfor an address of no known format or that nobody holds.Example:
lua---@param address string ---@return string? identifier ---@return string? coinId local identifier, coinId = exports["zyke_laptop"]:GetCryptoAddressOwner(address)
#Get Crypto Coins
Returns the listed coins in the app's order, with their current prices. A coin whose price is behind is still listed, with
stale = true.priceis money per coin andchangethe 24 hour move. Both arenilbefore the first price arrives.Example:
lua---@return {id: string, name: string, decimals: integer, price?: number, change?: number, updatedAt?: integer, stale: boolean, confirmations: integer, minAmount: number}[] coins local coins = exports["zyke_laptop"]:GetCryptoCoins()
#Get Crypto Price
Returns the money one coin is worth and whether the price is stale. Returns
nil, truefor a coin that isn't listed or before the first price arrives. Nothing is bought or sold, and no money amount is charged or paid, on a stale price.Example:
lua---@param coinId string ---@return number? price ---@return boolean stale local price, stale = exports["zyke_laptop"]:GetCryptoPrice("BSN")
exports["zyke_laptop"]:OpenApp("crypto", {coin = "BSN"}) on the client opens a coin's page in Satchel. The Discord logs use the CryptoTrade, CryptoTransfer and CryptoAdjusted actions; charges and payouts log as CryptoAdjusted.
#Payments
A payment belongs to an app or site your resource registered with Register App or Register Site. Your server makes it and decides the amount. Your page only passes its id to Laptop.pay from the JavaScript SDK, which opens the OS's pay sheet:
your server: CreatePayment(source, {...}) -> paymentId your page: Laptop.pay(paymentId) -> the OS's pay sheet, the player confirms laptop: takes the money, pays the payee -> zyke_laptop:PaymentSettled (payment) your server: delivers the order -> MarkPaymentFulfilled(paymentId) your page: the promise resolves -> {paymentId, status = "settled", method, amount, coin?}
Every payment export only sees the calling resource's payments.
#Lifecycle
| Status | Description |
|---|---|
pending | Made and waiting for the player. It can be paid, cancelled by you with CancelPayment or by the player on the sheet, and becomes expired once expiresIn runs out. A player without the money gets told on the sheet and the payment stays pending. |
settling | The player confirmed and the money is moving. |
settled | Paid. zyke_laptop:PaymentSettled fires. Deliver, then call MarkPaymentFulfilled. Refunds are only possible from here. |
failed | The money was taken but the payee couldn't be paid, so the player got it back. zyke_laptop:PaymentFailed fires with the code. |
cancelled | Cancelled while pending. |
expired | Not paid in time. |
review | The server stopped while it was settling, such as during a crash. The money may have moved without the payee getting it, so staff check it by hand. zyke_laptop:PaymentReview fires when the laptop starts again. |
fulfilled is a separate flag on a settled payment, set by MarkPaymentFulfilled, so you can find what was paid but not yet delivered. A refund fires zyke_laptop:PaymentRefunded with the amount and the reason. See Server Local Events for these events.
On the sheet the player pays from their bank or with Satchel's coins, as currency allows. Crypto needs Config.Sdk.payments.crypto and Satchel enabled. A crypto payment is a ChargeCrypto of the amount in money in the coin the player picked: the payment is settled at once, and its cryptoTransfer is the network transfer that confirms later. The player needs to be connected to a network to confirm, and CanMakeLaptopPayment in server/unlocked/hooks.lua can refuse it, for example while they are cuffed. The Discord logs use the SdkPayment, SdkRefund and SdkPaymentReview actions.
#Create Payment
Yields. Makes a payment for the player to confirm on the pay sheet and returns its id, such as
"pay_...". Returnsniland one of:
"disabled": payments are off, your resource isn't inConfig.Sdk.allowlist, orcurrency = "crypto"while crypto payments are off."notRegistered":appisn't an app or site your resource registered."invalid": a field isn't valid,linesdon't add up toamount, ormetadatais over 2048 bytes as JSON."payeeNotAllowed": the payee's kind is off inConfig.Sdk.payments.payees, or not in the app'spayments.payees."conflict": theidempotencyKeybelongs to a payment with a different amount, currency, payee or player.The same
idempotencyKeyreturns the same payment until it is cancelled, expires or fails. Then the key is free and the player can pay the same thing again.Example:
lua---@param plyId integer @ The player who pays ---@param options SdkPaymentOptions ---@return string? paymentId ---@return string? err local paymentId, err = exports["zyke_laptop"]:CreatePayment(source, { app = "fleeca_bank", amount = 2500, currency = "either", label = "Premium plan, 1 month", lines = {{label = "Premium plan", qty = 1, amount = 2500}}, payee = "society:fleeca", reference = "order-123", idempotencyKey = "order-123", expiresIn = 300, metadata = {plan = "premium"}, })
| Field | Default | Description |
|---|---|---|
app | Required | The id of the app or site the payment belongs to. Only its window can show the payment. |
kind | "app" | "app" or "site". |
amount | Required | Whole money, 1 up to Config.Sdk.payments.maxAmount (1000000). |
currency | "bank" | "bank", "crypto" or "either". |
label | Required | Shown on the sheet and in the player's history, up to 80 characters. |
lines | None | Up to 20 rows of {label, qty, amount} shown on the sheet. Labels are up to 80 characters, and the amounts must add up to the payment's. |
payee | "server" | "server" (the money leaves the economy), "society:<name>" or "player:<identifier>". player is off by default. A player payee who is offline is paid when they join next. |
reference | None | Your own reference, up to 64 characters. |
idempotencyKey | None | Up to 64 characters. See above. |
expiresIn | Config.Sdk.payments.expireSeconds (300) | Seconds the payment waits for the player, 30 to 3600. |
metadata | None | A table you get back on the payment, up to 2048 bytes as JSON. |
An app limits its payees with payments = {payees = {"society:fleeca"}} in its registration. A listed kind, such as "society", allows every payee of that kind.
#Get Payment
Yields. Returns one of your resource's payments, or
nilfor an unknown id or another resource's payment. A pending payment past its time reads asexpired.Example:
lua---@param paymentId string ---@return SdkPayment? payment local payment = exports["zyke_laptop"]:GetPayment(paymentId)
| Field | Description |
|---|---|
id | The payment id. |
resource / app | Your resource, and the app or site as "app:<id>" or "site:<id>". |
identifier | The character who pays. |
label, amount, currency, payee, reference, lines, metadata | As created. |
method | "bank" or "crypto", once the player picked one. |
coin / cryptoTransfer | The coin and the network transfer of a crypto payment. See Get Crypto Transfer. |
status | See Lifecycle. |
refunded | Money refunded so far. |
fulfilled | Whether MarkPaymentFulfilled was called. |
createdAt / expiresAt / settledAt | Unix times. |
#Get Payments
Yields. Returns your resource's payments, newest first, 100 at most. Every filter is optional. Use it after your resource restarted to deliver what was paid while it was down.
Example:
lua---@param filter? {status?: SdkPaymentStatus, fulfilled?: boolean, identifier?: string, since?: integer, limit?: integer} ---@return SdkPayment[] payments CreateThread(function() for _, payment in ipairs(exports["zyke_laptop"]:GetPayments({status = "settled", fulfilled = false})) do -- Deliver, then MarkPaymentFulfilled(payment.id) end end)
since is a Unix time payments were made at or after, and limit 1 to 100.
#Mark Payment Fulfilled
Yields. Marks a settled payment as delivered. Returns
falsefor a payment that isn't yours or isn't settled. Calling it again returnstrue.Example:
lua---@param paymentId string ---@return boolean marked AddEventHandler("zyke_laptop:PaymentSettled", function(payment) if (payment.resource ~= GetCurrentResourceName()) then return end -- Deliver payment.metadata.plan to payment.identifier exports["zyke_laptop"]:MarkPaymentFulfilled(payment.id) end)
#Cancel Payment
Yields. Cancels a payment while it is pending, such as when what it was for sold out. Returns
falsefor a payment that isn't yours or isn't pending any more.Example:
lua---@param paymentId string ---@return boolean cancelled local cancelled = exports["zyke_laptop"]:CancelPayment(paymentId)
#Refund Payment
Yields. Refunds a settled payment, all that is left of it when
amountis left out. Several partial refunds may add up to the payment's amount. Returnstrue, orfalseand one of:
"notFound": unknown, or another resource's payment."invalid": the payment isn't settled, the amount is 0 or more than is left, orreasonis over 128 characters."payeeFunds": a society payee can't cover the refund."failed": the money couldn't be paid back, such as to an offline player whileConfig.Sdk.payments.queueOfflineRefundsis off.A society payee pays the refund. A player payee keeps their money and the server pays it. The player gets it back the way they paid: to their bank, or as a
GiveCryptopayout of the amount at the coin's price now, which confirms after its blocks. A player who is offline gets a bank refund when they join next.zyke_laptop:PaymentRefundedfires.Example:
lua---@param paymentId string ---@param options? {amount?: integer, reason?: string} ---@return boolean refunded ---@return string? err local refunded, err = exports["zyke_laptop"]:RefundPayment(paymentId, {amount = 1000, reason = "Plan cancelled early"})