Documentation

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 enabled is true in shared/unlocked/crypto.lua. The payment exports need Config.Sdk.payments.enabled in shared/unlocked/sdk.lua; with it off they answer "disabled", nil, an empty list or false. When Config.Sdk.allowlist is 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

CryptoPaymentOptions and CryptoPaymentResult are in types/crypto.lua, and SdkPaymentOptions, SdkPayment and SdkPaymentStatus in types/sdk.lua inside zyke_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:

  • ChargeCrypto takes 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.
  • GiveCrypto puts 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.

OptionDefaultDescription
instantfalsetrue 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".
priorityThe 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. coinId is 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 true and a CryptoPaymentResult, or false and 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 fieldDescription
coinThe coin it was taken in.
units / amountThe amount in exact base units, and in coins.
fiatThe money amount asked for, rounded to whole money. nil for a coin amount without a fresh price.
feeUnits / feeThe network fee the player paid on top. 0 for payouts and instant charges.
txIdThe player's activity row.
transferIdThe network transfer. nil for an instant charge.
status"pending" until it confirms, "confirmed" for an instant charge.
confirmAtUnix time the transfer confirms.
confirmationsBlocks 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 true and a CryptoPaymentResult with txId empty until it confirms, or false and "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 its Confirmed event. status is "pending" or "confirmed". kind is "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, 0 for 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. total is the sum of the values in money. Coins on their way in aren't counted. Makes the wallet when the character has none, and returns nil for 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 nil for 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, nil for 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. price is money per coin and change the 24 hour move. Both are nil before 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, true for 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

StatusDescription
pendingMade 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.
settlingThe player confirmed and the money is moving.
settledPaid. zyke_laptop:PaymentSettled fires. Deliver, then call MarkPaymentFulfilled. Refunds are only possible from here.
failedThe money was taken but the payee couldn't be paid, so the player got it back. zyke_laptop:PaymentFailed fires with the code.
cancelledCancelled while pending.
expiredNot paid in time.
reviewThe 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_...". Returns nil and one of:

  • "disabled": payments are off, your resource isn't in Config.Sdk.allowlist, or currency = "crypto" while crypto payments are off.
  • "notRegistered": app isn't an app or site your resource registered.
  • "invalid": a field isn't valid, lines don't add up to amount, or metadata is over 2048 bytes as JSON.
  • "payeeNotAllowed": the payee's kind is off in Config.Sdk.payments.payees, or not in the app's payments.payees.
  • "conflict": the idempotencyKey belongs to a payment with a different amount, currency, payee or player.

The same idempotencyKey returns 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"},
})
FieldDefaultDescription
appRequiredThe id of the app or site the payment belongs to. Only its window can show the payment.
kind"app""app" or "site".
amountRequiredWhole money, 1 up to Config.Sdk.payments.maxAmount (1000000).
currency"bank""bank", "crypto" or "either".
labelRequiredShown on the sheet and in the player's history, up to 80 characters.
linesNoneUp 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.
referenceNoneYour own reference, up to 64 characters.
idempotencyKeyNoneUp to 64 characters. See above.
expiresInConfig.Sdk.payments.expireSeconds (300)Seconds the payment waits for the player, 30 to 3600.
metadataNoneA 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 nil for an unknown id or another resource's payment. A pending payment past its time reads as expired.

Example:

lua
---@param paymentId string
---@return SdkPayment? payment
local payment = exports["zyke_laptop"]:GetPayment(paymentId)
FieldDescription
idThe payment id.
resource / appYour resource, and the app or site as "app:<id>" or "site:<id>".
identifierThe character who pays.
label, amount, currency, payee, reference, lines, metadataAs created.
method"bank" or "crypto", once the player picked one.
coin / cryptoTransferThe coin and the network transfer of a crypto payment. See Get Crypto Transfer.
statusSee Lifecycle.
refundedMoney refunded so far.
fulfilledWhether MarkPaymentFulfilled was called.
createdAt / expiresAt / settledAtUnix 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 false for a payment that isn't yours or isn't settled. Calling it again returns true.

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 false for 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 amount is left out. Several partial refunds may add up to the payment's amount. Returns true, or false and one of:

  • "notFound": unknown, or another resource's payment.
  • "invalid": the payment isn't settled, the amount is 0 or more than is left, or reason is 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 while Config.Sdk.payments.queueOfflineRefunds is 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 GiveCrypto payout 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:PaymentRefunded fires.

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"})