Bitcoin Payment Acceptance Setup

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Bitcoin Payment Acceptance Setup
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Setting Up Bitcoin Payment Acceptance

The most common mistake when integrating Bitcoin payments is using a single address for all users and tracking amounts. This doesn't work: two users can send the same amount in one transaction, you can receive multiple incomplete payments (UTXOs) that together constitute the needed amount, or a user might send from an exchange using batching. The correct approach is to generate a unique address for each payment.

Architecture: HD Wallets and Address Derivation

The BIP-32/BIP-44 standard allows generating an infinite tree of addresses deterministically from a single master seed. For payment acceptance, use the xpub (extended public key) — the public part that the server stores openly and uses to generate addresses. The private key is stored separately (cold storage, hardware wallet) and is needed only for withdrawals.

Master Seed → xpub (m/44'/0'/0')
             ↓
  index=0: 1A1zP1... (payment #1)
  index=1: 1B2zP2... (payment #2)
  index=N: ...       (payment #N)

Derivation path per BIP-44 for Bitcoin mainnet: m/44'/0'/account'/change/index. For receiving — change=0, increment index.

Address Types

Type Format SegWit Recommended
P2PKH (Legacy) 1... No No (high fees)
P2SH-P2WPKH (Wrapped SegWit) 3... Yes For compatibility
P2WPKH (Native SegWit) bc1q... Yes Yes
P2TR (Taproot) bc1p... Yes Yes (new projects)

I recommend Native SegWit (bc1q): fees are 30–40% lower than Legacy, supported by all modern wallets and exchanges. Taproot — if you need Schnorr signatures and scripts in the future.

Implementation: Node.js + bitcoinjs-lib

npm install bitcoinjs-lib bip32 bip39 tiny-secp256k1
import * as bitcoin from 'bitcoinjs-lib'
import { BIP32Factory } from 'bip32'
import * as ecc from 'tiny-secp256k1'

bitcoin.initEccLib(ecc)
const bip32 = BIP32Factory(ecc)

const NETWORK = bitcoin.networks.bitcoin // or networks.testnet

// Once: generate xpub from seed (execute in cold storage)
// const seed = bip39.mnemonicToSeedSync(mnemonic)
// const root = bip32.fromSeed(seed, NETWORK)
// const account = root.derivePath("m/84'/0'/0'") // BIP-84 for Native SegWit
// const xpub = account.neutered().toBase58()
// console.log(xpub) // store in .env as BITCOIN_XPUB

// On the server: generate address by index
function getPaymentAddress(xpub: string, index: number): string {
  const node = bip32.fromBase58(xpub, NETWORK)
  const child = node.derive(0).derive(index) // external chain, index N
  const { address } = bitcoin.payments.p2wpkh({
    pubkey: Buffer.from(child.publicKey),
    network: NETWORK,
  })
  if (!address) throw new Error('Failed to derive address')
  return address
}

Database: Payment Schema

CREATE TABLE bitcoin_payments (
  id          BIGSERIAL PRIMARY KEY,
  order_id    UUID NOT NULL REFERENCES orders(id),
  address     VARCHAR(62) NOT NULL UNIQUE,
  hd_index    INTEGER NOT NULL UNIQUE,
  amount_sat  BIGINT NOT NULL,           -- amount in satoshis
  status      VARCHAR(20) DEFAULT 'pending', -- pending/underpaid/confirmed/expired
  created_at  TIMESTAMPTZ DEFAULT NOW(),
  expires_at  TIMESTAMPTZ NOT NULL,
  confirmed_at TIMESTAMPTZ,
  tx_hash     VARCHAR(64)
);

CREATE INDEX ON bitcoin_payments(address);
CREATE INDEX ON bitcoin_payments(status) WHERE status = 'pending';

Transaction Monitoring

Option 1: Electrum/Electrs via WebSocket

The cheapest way — run electrs (Rust implementation of Electrum Server) on top of your Bitcoin node and listen to events:

import ElectrumClient from 'electrum-client'

const client = new ElectrumClient(50002, 'your-electrs-host', 'tls')
await client.connect('payment-monitor', '1.4')

// Subscribe to address
async function watchAddress(address: string, onPayment: (tx: any) => void) {
  const scriptHash = addressToScriptHash(address) // sha256 reversedLE
  await client.subscribe.on('blockchain.scripthash.subscribe', async (updates) => {
    const [scripthash, status] = updates
    if (scripthash === scriptHash && status !== null) {
      const history = await client.blockchainScripthash_getHistory(scriptHash)
      onPayment(history)
    }
  })
  await client.blockchainScripthash_subscribe(scriptHash)
}

Option 2: Polling via Public API

For MVP or low load — Blockstream Esplora API (public, no key):

async function checkPayment(address: string, expectedSat: bigint): Promise<'pending' | 'underpaid' | 'confirmed'> {
  const res = await fetch(`https://blockstream.info/api/address/${address}/utxo`)
  const utxos: Array<{ txid: string; value: number; status: { confirmed: boolean } }> = await res.json()

  const confirmedSat = utxos
    .filter(u => u.status.confirmed)
    .reduce((sum, u) => sum + BigInt(u.value), 0n)

  if (confirmedSat === 0n) return 'pending'
  if (confirmedSat < expectedSat) return 'underpaid'
  return 'confirmed'
}

For production use your own node + electrs. Dependency on public APIs in a payment service — not okay.

Number of Confirmations

Amount Recommended Confirmations
< $100 1 (first block inclusion)
$100 – $1 000 3
$1 000 – $10 000 6
> $10 000 6+ or wait for Finalized by business logic

0-conf (unconfirmed) is acceptable only for physical point-of-sale with small amounts and RBF=false. In e-commerce — only with confirmations.

Edge Case Handling

Overpayment — user sent more. Policy: credit as full payment, keep the difference on balance, offer refund or credit toward next order. Auto-refund — only if you have a refund address from the user.

Underpayment — less than needed arrived. Two options: freeze the payment and ask for the difference to the same address (within time window); or cancel and request new payment. Don't credit partial payment as full.

Expiry — address expired (usually in 15–60 minutes) but transaction still arrived. Strategy: keep "expired" addresses active for 24 more hours but don't show to user for new payments.

Transaction Replacement (RBF) — transaction can be replaced with higher fee. Don't trust unconfirmed transactions with BIP125-opt-in-RBF=true flag.

Withdrawals

To withdraw accumulated UTXOs you need coin selection logic. Use bitcoinjs-lib PSBT:

// Access to private keys needed — execute only in isolated service
const psbt = new bitcoin.Psbt({ network: NETWORK })
// ... add inputs/outputs with correct fee calculation
// Transaction size in vbytes depends on number of inputs/outputs
// fee = feeRate (sat/vByte) * vsize

I recommend sweeping periodically (once a day) via script, not automatically on each payment — this reduces transaction count and fees.