Extensions
/
NowPayments
NOWPayments 1.0.0
Crypto top-ups through NOWPayments. The customer picks an amount in the bot's currency and a coin, gets a deposit address and the exact crypto amount, and the wallet is credited once NOWPayments reports the payment finished. The bot-currency-to-USD rate is read from a URL you set.
Install
Install it
php bin/console ext:install NowPayments
Update it later
php bin/console ext:update NowPayments
Remove it
php bin/console ext:remove NowPayments
Add --dry-run to any of these to see exactly which files
would change before anything is written.
Readme
NowPayments
Crypto top-ups through [NOWPayments](https://nowpayments.io). A customer
picks an amount in the bot's currency and a coin, gets a deposit address
and the exact amount of coin to send, and the wallet is credited
automatically once NOWPayments reports the payment finished.
It registers itself as a payment method (nowpayments) with core's
top-up system, so it shows up in /topup, can be switched off from
/admin โ Top-ups โ ๐ณ Payment methods, and the money it brings in is
attributed to it on the financial report.
Installing
php bin/console ext:install NowPayments
Then configure it from the console. Three settings are needed:
# Your NOWPayments API key (Dashboard โ Settings โ Payments โ API keys)
php bin/console ext:set NowPayments api_key "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX"
# An https link that answers with how many units of the bot's currency 1 USD is worth
php bin/console ext:set NowPayments rate_url "https://example.com/usd-rate"
# Which coins to offer (NOWPayments tickers, comma-separated)
php bin/console ext:set NowPayments coins "usdttrc20,trx,ton,ltc"
Check what is set with php bin/console ext:settings NowPayments. Note
that this prints the API key in full. If you would rather keep it off
screen, leave api_key empty and put NOWPAYMENTS_API_KEY=... in .env;
the setting wins when both are present.
Then switch the method on: /admin โ Top-ups โ ๐ณ Payment methods.
A new payment method is off until somebody turns it on. The line under its
name says what is missing while it is not configured.
Finally, make sure the job worker is running (`php bin/console
jobs:work, or jobs:work --run-service` under systemd). The worker is what
notices a payment has arrived. Without it, payments are still credited, but
only when the customer presses ๐ Check payment.
The coins you list must also be enabled in your NOWPayments dashboard
(Coins settings), or creating a payment in them fails.
The rate URL
rate_url must be an https:// link. A plain GET to it must answer with
a whole number: how many units of WALLET_CURRENCY one US dollar is
worth. With WALLET_CURRENCY=IRT, a body of
105000
means 1 USD = 105,000 IRT. The number is in whole units of the wallet
currency whatever WALLET_SCALE is.
A few other shapes are accepted, so an existing price API can be used
without a proxy in front of it:
| Body | rate_key |
| --- | --- |
| 105000 | (empty) |
| 105,000 or 105000.4 (rounded) | (empty) |
| {"rate": 105000} | rate |
| {"data": {"usd": "105000"}} | data.usd |
php bin/console ext:set NowPayments rate_key "data.usd"
Zero, a negative number, an HTML page or anything else unreadable is
refused, never treated as a default. If the URL is down, customers are
told crypto payments are unavailable right now and a warning is logged.
The rate is read when the customer chooses this method, and that same
number is used for the limits they are shown and the price they are
charged. If they take longer than rate_max_age seconds (default 900) to
finish, the rate is read again when the payment is created.
What the customer sees
User: /topup โ ๐ช Crypto
Bot: ๐ช Top up with crypto
๐ฑ Rate: 1 USD = 105,000 IRT
โฌ๏ธ Minimum: 105,000 IRT
โฌ๏ธ Maximum: 105,000,000 IRT
How much do you want to add to your wallet? Send the amount in IRT.
User: 1050000
Bot: ๐ต 1,050,000 IRT โ $10.00
Which coin will you pay with?
[USDT ยท TRC20] [TRON] [Toncoin] [Litecoin]
User: [USDT ยท TRC20]
Bot: ๐ช Crypto payment #12 ยท USDT ยท TRC20
1๏ธโฃ Send exactly:
10.012345 USDTTRC20
๐ Network: TRX
2๏ธโฃ To this address:
TXYZ...
๐ต $10.00 โ 1,050,000 IRT
โณ Pay before: 2026-10-29 13:05 UTC
๐ Status: โณ Waiting for your payment
[๐ Address] [๐ Amount]
[๐ Check payment]
The amount can be typed with Persian digits and thousands separators.
Whole units only.
That same message updates itself as the payment moves: confirming,
paid, expired. When the money is credited, the customer also gets a
separate message with the amount and their new balance. Coins that need a
memo or destination tag (XRP, XLM, ...) show it with a warning, plus a
copy button for it.
The wallet is credited with the amount the customer asked for, in the
bot's currency, at the rate locked in when the payment was created. If
the dollar moves while the payment is open, that changes nothing for this
customer.
Statuses
| NOWPayments says | What happens |
| --- | --- |
| waiting | keep watching; closed if still unpaid 15 min after "Pay before" |
| confirming, confirmed, sending | keep watching closely (credited at confirmed if credit_on_confirmed is on) |
| finished | credited in full |
| partially_paid | see below |
| failed, refunded, expired | closed; the customer is told if coins may have moved |
Underpayments. NOWPayments advises against delivering on
partially_paid, so by default nothing is credited. The customer is told
that support will sort it out, and every admin in ADMIN_IDS gets a
message with the payment details so they can credit it by hand
(php bin/console wallet:credit <telegram-id> <amount> <reason>). Set
credit_partial to true to credit pro rata instead: paying 40% of the
coin amount credits 40% of the ordered amount, rounded down.
Overpaying credits the ordered amount. The extra stays in your NOWPayments
balance.
A payment that has coins in flight is watched for up to 7 days. If it
still has no final status then, it is closed and the admins are told to
check it in the NOWPayments dashboard.
Settings
| Key | Default | Meaning |
| --- | --- | --- |
| api_key | "" | NOWPayments API key; falls back to NOWPAYMENTS_API_KEY in .env |
| rate_url | "" | https link answering with bot-currency units per 1 USD |
| rate_key | "" | dot path to the rate when rate_url answers with a JSON object |
| rate_max_age | 900 | seconds the rate the customer saw stays valid (60โ86400) |
| coins | usdttrc20,trx,ton,ltc,btc,eth | coins offered, NOWPayments tickers, max 12 |
| min_usd | 1 | smallest price in USD, decimals allowed (2.5) |
| max_usd | 1000 | largest price in USD; 0 = no ceiling |
| min_amount | 0 | extra floor in wallet minor units; 0 = only min_usd |
| max_amount | 0 | extra ceiling in wallet minor units; 0 = only max_usd |
| max_open | 3 | unpaid payments one customer may have open at once (1โ10) |
| fixed_rate | false | NOWPayments fixed-rate payment; the customer has only 10 minutes to pay |
| fee_paid_by_user | false | network fees added to what the customer pays; implies fixed_rate |
| credit_on_confirmed | false | credit at confirmed instead of waiting for finished |
| credit_partial | false | credit underpayments pro rata instead of handing them to an admin |
| watch_hours | 48 | hours an unpaid payment is watched (1โ168); NOWPayments' own expiry wins if sooner |
| poll_seconds | 60 | how often fresh payments are checked (30โ3600); older ones are checked less often |
| timeout | 15 | seconds to wait on NOWPayments or the rate URL (3โ30) |
| sandbox | false | use api-sandbox.nowpayments.io (needs a sandbox API key) |
| support_contact | "" | e.g. @yoursupport, appended to messages that tell the customer to ask support |
The minimum each coin accepts is checked with NOWPayments before a payment
is created. If the price is below it, the customer is told the minimum
for that coin instead of being given an address that would never confirm.
How it works, and why
No webhook. NOWPayments can send IPN callbacks, but an extension has no
public HTTP endpoint; the only one this bot has is Telegram's. So payments
are created with Create payment (POST /v1/payment) and polled with
Get payment status (GET /v1/payment/{id}). Both need only the API key.
The hosted-invoice flow was not used: checking an invoice's payments needs
the account's email and password (JWT), which this bot should not hold.
One poll job. NowPayments:poll is a single keyed, repeating job,
armed by the first payment. Fresh payments are checked every
poll_seconds; ones still unpaid after an hour every 5 minutes; after six
hours every 15. Payments with coins in flight are always checked at the
fast pace.
Money moves once. A payment is credited only by whoever moves its row
out of open with a conditional UPDATE. That happens in the same
transaction as the credit, which goes through TopUpService::credit()
with the idempotency key nowpayments:<payment_id>:credit. The poll job
and a customer pressing Check can both see finished at the same
moment: one credits, the other finds nothing to do. If the credit fails,
the transaction rolls back, the row is open again, and the next check
retries under the same key.
Order ids. Each payment is sent to NOWPayments with order id
botex-np-<row id>, so anything in the NOWPayments dashboard can be
traced back to a row here, and from there to a customer.
Tables
| Table | Holds |
| --- | --- |
| nowpayments_payments | one row per payment, from creation to credit |
uninstall() drops it. Wallet entries written by a payment are left
alone. They are referenced topup:nowpayments#<row id> in the ledger.
Releases
| Version | Published | Size | Files |
|---|---|---|---|
| 1.0.0 current | 29 Oct 2026 | 39.7 KB | 21 |