BNB Payment Acceptance Setup
BNB operates in two contexts that are easy to confuse: native BNB on BNB Chain (BSC, chainId 56) and BEP-2 BNB on Binance Chain (the old chain, practically obsolete). In 99% of cases the customer wants to accept BNB on BSC — that's exactly what we'll cover.
Approach 1: Without Third-Party Providers
The most straightforward option — monitoring incoming transactions to an address via BSC node or public RPC.
Generate unique address for each order via HD wallet:
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://bsc-dataseed1.binance.org/");
// From mnemonic — for development. In production — xpub without storing seed
const masterWallet = ethers.Wallet.fromPhrase(process.env.MNEMONIC!);
function getDepositAddress(orderId: number): string {
return masterWallet.deriveChild(orderId).address;
}
async function waitForPayment(address: string, expectedAmount: bigint): Promise<string> {
return new Promise((resolve) => {
provider.on({ address }, (tx) => {
if (tx.value >= expectedAmount) {
resolve(tx.hash);
}
});
});
}
Confirmations: on BSC with 21 validators and ~3-second blocks — wait minimum 15 confirmations for amounts up to $1000, 30+ for large ones. Reorgs on BSC are rare but do happen.
Polling verification (more reliable than websocket subscriptions on public RPC):
async function checkPayment(
depositAddress: string,
expectedWei: bigint,
fromBlock: number
): Promise<boolean> {
const currentBlock = await provider.getBlockNumber();
for (let block = fromBlock; block <= currentBlock; block++) {
const blockData = await provider.getBlock(block, true);
const incoming = blockData?.transactions.filter(
(tx: any) =>
tx.to?.toLowerCase() === depositAddress.toLowerCase() &&
BigInt(tx.value) >= expectedWei
);
if (incoming && incoming.length > 0) return true;
}
return false;
}
Approach 2: Via BscScan API
Simpler, doesn't require your own node, but adds dependency on third-party service:
async function getIncomingBNB(address: string, startBlock: number) {
const url = `https://api.bscscan.com/api?module=account&action=txlist` +
`&address=${address}&startblock=${startBlock}&endblock=99999999` +
`&sort=asc&apikey=${process.env.BSCSCAN_API_KEY}`;
const { result } = await fetch(url).then(r => r.json());
return result.filter((tx: any) =>
tx.to.toLowerCase() === address.toLowerCase() &&
tx.isError === "0"
);
}
BscScan free plan limit: 5 requests/sec, 100k requests/day — sufficient for small volumes.
Accepting USDT/USDC on BEP-20
Often need to accept not just native BNB, but stablecoins on BSC. USDT BEP-20: 0x55d398326f99059fF775485246999027B3197955. Monitoring via Transfer events:
const USDT_BSC = "0x55d398326f99059fF775485246999027B3197955";
const transferInterface = new ethers.Interface([
"event Transfer(address indexed from, address indexed to, uint256 value)"
]);
const filter = {
address: USDT_BSC,
topics: [
ethers.id("Transfer(address,address,uint256)"),
null,
ethers.zeroPadValue(depositAddress, 32),
],
};
provider.on(filter, (log) => {
const { from, to, value } = transferInterface.parseLog(log)!.args;
console.log(`Received ${ethers.formatUnits(value, 18)} USDT from ${from}`);
});
Sweep and Fund Collection
After payment confirmation, the funds need to be transferred to the main cold/hot wallet. For native BNB this is a simple transaction. Keep in mind that for sweep you need to leave BNB for gas (~0.0005 BNB at 3 gwei gasPrice and 21000 gas limit).
For automatic sweep of derived wallets you need a cron worker that checks balances of deposit addresses and transfers accumulated funds.
Time Estimates
Basic setup for accepting native BNB with webhook notification: 1 day. Adding BEP-20 tokens (USDT, USDC) and auto-sweep: another 1–2 days. Integration into existing backend depends on its architecture.







