Crypto Payments
Four libraries accept crypto payments: NOWPayments (receive-only, decimal amounts, hosted invoices), Apirone (wallet/account model, smallest units), and the hosted gateways OxaPay and Cryptomus (decimal amounts, hosted pay pages). If a brief just says "crypto" or "payment," default to NOWPayments.
For in-app purchases paid in Telegram Stars (no external gateway), use send.invoice with the on pre_checkout and on successful_payment triggers โ no library needed.
Keep API keys and IPN secrets in global.* variables (set in the Variables panel). Never hardcode them. Pass them as parameters to the library calls.
NOWPaymentsโ
import nowpayments from "nowpayments"
Receive-only: create payments and invoices, then confirm them via a verified IPN webhook. Every call takes api_key (sent as the x-api-key header). Amounts are decimal in the named currency.
Subflowsโ
| Subflow | Signature (key params) โ outputs |
|---|---|
get_status | (api_key) โ {ok, message} |
get_currencies | (api_key) โ {currencies} |
get_available_currencies | (api_key) โ {currencies} |
min_amount | (api_key, currency_from, currency_to) โ {min_amount, โฆ} |
estimate_price | (api_key, amount, currency_from, currency_to) โ {estimated_amount, โฆ} |
create_payment | (api_key, price_amount, price_currency, pay_currency, order_id, order_description, ipn_callback_url) โ {payment_id, pay_address, pay_amount, pay_currency, payment_status} |
create_invoice | (api_key, price_amount, price_currency, order_id, order_description, ipn_callback_url, success_url, cancel_url) โ {invoice_id, invoice_url} |
create_invoice_payment | (api_key, invoice_id, pay_currency, order_description) โ {payment_id, pay_address, pay_amount, โฆ} |
get_payment_status | (api_key, payment_id) โ {payment_status, actually_paid, pay_amount, price_amount, outcome_amount, order_id} |
handle_ipn | (api_key, payment_id, reported_status) โ {confirmed, finished, status, payment_status, actually_paid, pay_amount, price_amount, order_id} |
Receive a paymentโ
import nowpayments from "nowpayments"
bot Shop {
global nowpayments_api_key: String = ""
global nowpayments_ipn_secret: String = ""
user last_payment_id: Number = 0
user pending_plan: String = ""
flow buy on command "/buy" {
// Encode the buyer's id into order_id so the IPN can route the unlock.
set { flow.order_id = `${user.id}-monthly` }
let { payment_id, pay_address, pay_amount, pay_currency } = await nowpayments.create_payment(
api_key: global.nowpayments_api_key,
price_amount: 9.99,
price_currency: "usd",
pay_currency: "btc",
order_id: flow.order_id,
order_description: "Monthly subscription",
ipn_callback_url: `${bot.webhook_base}nowpayments`
) on error {
send.text("โ Couldn't start a payment. Try again shortly.")
stop
}
set { user.last_payment_id = payment_id }
send.text(`๐งพ Send *${pay_amount} ${pay_currency}* to:\n\`${pay_address}\`\n\nYou'll unlock automatically once it confirms.`, parse_mode: "Markdown")
}
}
Confirm via IPN webhookโ
NOWPayments signs IPNs with HMAC-SHA512 over sorted JSON. Re-verify with handle_ipn (trust-but-verify) and deliver only when finished:
flow nowpayments_ipn on webhook("nowpayments",
verify: hmac, sig_algo: sha512, sig_payload: sorted_json,
header: "x-nowpayments-sig", secret: global.nowpayments_ipn_secret,
methods: [post], bind: request.body.order_id
) with body: { payment_id: Number, payment_status: String, order_id: String } {
let { finished, status, order_id } = await nowpayments.handle_ipn(
api_key: global.nowpayments_api_key,
payment_id: request.body.payment_id,
reported_status: request.body.payment_status
) on error {
respond.json({ ok: true }) // still ack so NOWPayments stops retrying
stop
}
when !finished {
respond.json({ ok: true })
stop
}
// Recover the buyer's chat id encoded in order_id ("<uid>-<plan>").
set { flow.buyer_uid = types.int(str.split(order_id, "-")[0]) }
send.text("โ
Payment received โ access unlocked!", chat_id: flow.buyer_uid)
respond.json({ ok: true })
}
payment_status values: waiting, confirming, confirmed, sending, partially_paid, finished, failed, expired, refunded. Deliver only on finished.
NOWPayments IPNs carry order_id but no arbitrary data echo. Encode routing (like the chat id) into order_id and parse it back, or persist an order_id โ chat_id map at create time.
Reference blueprint: nowpayments_shop.botgami.
Apironeโ
import apirone from "apirone"
Wallet/account model. Amounts are in smallest units: BTC/LTC/BCH/DOGE โ satoshi, TRX โ sun.
Subflowsโ
| Subflow | Signature (key params) โ outputs |
|---|---|
create_wallet | (currency) โ {wallet, transfer_key} โ transfer_key is returned once; store it immediately |
new_address | (wallet, callback_url, chat_id, order_id, secret) โ {address} |
account_new_address | (account, currency, callback_url, chat_id, order_id, secret) โ {address} |
wallet_balance | (wallet) โ {available, total} |
address_balance | (wallet, address) โ {available, total} |
account_address_balance | (account, address) โ {available, total} |
payout | (wallet, transfer_key, address, amount) โ {id, txid} |
create_invoice | (account, currency, amount, callback_url) โ {invoice, address, invoice_url, status} |
list_currencies | () โ {networks} |
handle_callback | (account, address, reported_value, confirmations, order_id, chat_id) โ {confirmed, amount, address, order_id, chat_id, status} |
Generate a deposit addressโ
import apirone from "apirone"
bot Wallet {
global apirone_secret: String = ""
global wallet_id: String = ""
flow pay on command "/pay" {
let { address } = await apirone.new_address(
wallet: global.wallet_id,
callback_url: `${bot.webhook_base}payment_cb`,
chat_id: user.id,
order_id: `${user.id}-access`,
secret: global.apirone_secret
) on error {
send.text("โ Couldn't generate an address. Try again.")
stop
}
send.text(`Send BTC to:\n\`${address}\``, parse_mode: "Markdown")
}
}
Confirm via callback webhookโ
Apirone callbacks carry no HMAC signature โ they echo back the data object you supplied. Use verify: data and respond with exactly *ok* so Apirone stops retrying:
flow payment_cb on webhook("payment_cb",
verify: data,
secret: global.apirone_secret,
data_field: "data.secret",
methods: [post]
) with body: {
value: Number,
confirmations: Number,
data: { chat_id: Number, order_id: String, secret: String }
} {
respond.text("*ok*")
let { confirmed, status } = await apirone.handle_callback(
account: global.wallet_id,
address: "",
reported_value: request.body.value,
confirmations: request.body.confirmations,
order_id: request.body.data.order_id,
chat_id: request.body.data.chat_id
) on error {
stop
}
when confirmed {
send.text(`โ
Payment of ${request.body.value} sat received!`, chat_id: request.body.data.chat_id)
}
}
Reference blueprints: crypto_wallet_bot.botgami, payment_concierge.botgami.
OxaPayโ
import oxapay from "oxapay"
A receive-only wrapper around the OxaPay Merchant API. Amounts are decimal in the named currency. Pass api_key (the merchant API key) as a parameter.
Subflowsโ
| Subflow | Signature (key params) โ outputs |
|---|---|
create_invoice | (api_key, amount, currency, order_id, callback_url, description) โ {track_id, payment_url} |
payment_info | (api_key, track_id) โ {status, paid} |
handle_callback | (api_key, track_id) โ {paid, status} |
import oxapay from "oxapay"
bot Shop {
global oxapay_api_key: String = ""
flow buy on command "/buy" {
set { flow.order_id = `${user.id}-pro` }
let { payment_url } = await oxapay.create_invoice(
api_key: global.oxapay_api_key,
amount: 19.99, currency: "USD",
order_id: flow.order_id,
callback_url: `${bot.webhook_base}oxapay`,
description: "Pro plan"
) on error {
send.text("โ Couldn't start a payment.")
stop
}
send.text(`๐ณ Pay here:\n${payment_url}`)
}
}
OxaPay POSTs status updates to callback_url. Wire a webhook and re-verify with handle_callback (trust-but-verify) before delivering goods.
Cryptomusโ
import cryptomus from "cryptomus"
Receive crypto via the Cryptomus Merchant API (100+ coins). Amounts are strings (e.g. "19.99"). Pass both api_key and merchant_id as parameters.
Subflowsโ
| Subflow | Signature (key params) โ outputs |
|---|---|
create_invoice | (api_key, merchant_id, amount, currency, order_id, url_callback) โ {uuid, url, status} |
payment_info | (api_key, merchant_id, uuid) โ {status, paid} |
handle_webhook | (api_key, merchant_id, uuid) โ {confirmed, status} |
import cryptomus from "cryptomus"
bot Shop {
global cryptomus_api_key: String = ""
global cryptomus_merchant: String = ""
flow buy on command "/buy" {
set { flow.order_id = `${user.id}-pro` }
let { url } = await cryptomus.create_invoice(
api_key: global.cryptomus_api_key,
merchant_id: global.cryptomus_merchant,
amount: "19.99", currency: "USD",
order_id: flow.order_id,
url_callback: `${bot.webhook_base}cryptomus`
) on error {
send.text("โ Couldn't start a payment.")
stop
}
send.text(`๐ณ Pay here:\n${url}`)
}
}
Cryptomus POSTs to url_callback. Re-verify with handle_webhook before delivering goods.