B Botex Archive

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.

stable 1 installs

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