logo

Integrating a Bitcoin Wallet With bitcoinjs-lib: From “Generate Keys” to “Ship Transactions”

Digital ledger illustrating blockchain technology that records and stores transaction information securely.

A Bitcoin wallet integration can look deceptively simple at first glance.

You derive a key, generate an address, and call it a day. Then you try to show a user’s “balance” and discover you can’t, because Bitcoin does not have balances in the account sense. Then you try to send funds and discover you can’t, because signing needs UTXO details you never stored. Then you attempt to support a second address and discover you’ve accidentally built an address reuse machine.

bitcoinjs-lib is a strong toolkit for Bitcoin primitives, yet it won’t design the wallet for you. It gives you the building blocks, and your job is to assemble them into a predictable pipeline that covers keys, addresses, UTXO tracking, transaction creation, and signing. The moment you view this as a system, the integration starts to behave like a real wallet instead of a demo.

The mental model shift: Accounts are trees, balances are UTXOs

Ethereum-trained teams often expect “an account has a balance.” Bitcoin works differently. Funds live in unspent transaction outputs (UTXOs), and a wallet’s spendable amount is the sum of UTXOs controlled by the wallet’s scripts, minus what you decide to treat as reserved for fees.

Your key management usually ends up hierarchical. HD wallets derive many keys from one seed, so you can keep generating fresh receive addresses without juggling a pile of backups. That approach is formalized in BIP-32, which defines hierarchical deterministic wallets and the ability to derive a key tree from a master secret.

This means “integrating a wallet” has two parallel threads:

  • Key and address generation that stays compatible across tools and restores cleanly.
  • UTXO discovery and tracking that stays accurate across reorgs, mempool activity, and multiple addresses.

bitcoinjs-lib sits mostly on the first thread and on transaction building. It doesn’t replace the second thread. You’ll still need a chain data source to discover UTXOs and broadcast transactions.

Wallet data choices that decide compatibility early

Most wallet pain comes from decisions that were implicit on day one. The best time to make them explicit is before you generate the first address, because you’ll carry them into every restore flow.

Address type and derivation path choices

Address type choices change script templates, fee behavior, and compatibility with other wallets. In modern integrations, native SegWit (P2WPKH) is a common baseline because it reduces weight compared to legacy scripts. Derivation conventions for native SegWit accounts are standardized in BIP-84.

Once you pick an address type, your derivation path follows. BIP-32 gives you the HD mechanism, and BIP-84 gives you a commonly expected scheme for P2WPKH account structure.

If you want a simple compatibility rule, keep the address type and derivation path stable for a given wallet profile. Mixing types across the same seed is possible, yet it complicates discovery and UI, and it often turns restores into a scavenger hunt.

Network selection across mainnet, testnet, regtest

Bitcoin networks share the same core data model and differ in parameters and human expectations. A practical integration makes the network an explicit setting that influences address prefixes, explorers, and fee estimation sources.

bitcoinjs-lib supports different networks through parameters rather than magic defaults, which is helpful because it pushes you to be deliberate. Its README also emphasizes that crypto work depends on a reliable RNG and that you should verify behavior in your target environment.

Watch-only boundaries and xpub risk

Watch-only is tempting because it simplifies servers. You derive public addresses from an extended public key and track deposits without holding private keys. BIP-32 supports partial sharing through extended keys, which is why xpub-based monitoring exists in the first place.

The catch is that xpub exposure is not a harmless convenience. The bitcoinjs-lib maintainers explicitly warn that sharing BIP-32 extended public keys can be a liability, especially if any private key leaks or implementation bugs exist.

A practical approach is to treat xpub handling as sensitive data. Keep it scoped and monitored. Avoid casual logging. Plan a rotation story for the “watch-only identifier” if your system design expects long-lived monitoring.

A practical integration path in five steps

Step 1: Seed and keychain setup

Most modern wallets start from a mnemonic. BIP-39 defines mnemonic generation and conversion to a binary seed that can then feed BIP-32 key derivation.

Your integration decision here is simple: where does the seed live, and how do you protect it. If you are building a non-custodial wallet inside an app, keep the seed material inside secure storage primitives appropriate to the platform. If you are building an embedded wallet where keys are held server-side, treat the seed as HSM-grade material and avoid handling it in application memory.

A small implementation detail often missing in quick guides is normalization. BIP-39 expects deterministic processing from mnemonic to seed, which makes input handling and passphrase behavior security-relevant.

Step 2: Address derivation and gap-limit tracking

Once you have an HD root, you’ll derive addresses along a path, commonly following BIP-84 for native SegWit wallets.

Now you face a wallet reality problem: address discovery. You can’t scan infinite addresses, and you shouldn’t reuse the same one. Production wallets typically implement a “gap limit” strategy: derive a window of addresses, scan for activity, and extend the window as soon as you see usage.

This is where many integrations fail quietly. They derive a single address and call it “the wallet.” That behaves like a donation address, not a wallet. bitcoinjs-lib issues and community discussions are full of questions that start with “why do I always get the same address,” which is often a sign that the derivation index never changes.

Step 3: UTXO sync and balance calculation

bitcoinjs-lib does not run a node or fetch UTXOs for you. You’ll typically integrate a chain data source such as an Electrum server, an Esplora API, or your own node indexer. The wallet’s “balance” becomes a computed view derived from UTXOs tied to your addresses.

A production-ready balance is usually split into at least two concepts:

  • Confirmed balance from UTXOs in confirmed blocks.
  • Pending balance impact from mempool transactions that spend your UTXOs or pay into your addresses.

If you skip mempool awareness, users will see funds “disappear” right after they press send. The UTXO was selected and spent, and until the transaction confirms, explorers and wallets vary in how they present it. Your UI should match your backend’s model rather than relying on vibes.

Step 4: PSBT construction and signing

PSBT exists because Bitcoin transactions are often assembled and signed across different components. The original PSBT format is specified in BIP-174, and it’s widely used for offline signing and multi-party workflows.

bitcoinjs-lib includes a Psbt class and is commonly used to create and sign PSBTs. The bitcoinjs ecosystem even maintains a dedicated BIP-174 encoding library and points Bitcoin users back to bitcoinjs-lib’s Psbt for practical usage.

Here is the integration truth that tends to surprise teams: signing needs input data. At a minimum, a signer needs enough information about each input being spent to produce a valid signature, which is exactly why PSBT exists as a container. bitcoinops.org highlights PSBT as the mechanism that carries the data required to sign, and that BIP-174 defines PSBT v0, with later evolution in BIP-370.

Step 5: Finalization and broadcast

After signatures are added, you finalize the PSBT into a fully signed transaction and broadcast it through your chosen backend. Broadcasting is a separate concern from signing. It needs retry handling, idempotency, and transaction tracking.

If you aim for user trust, treat “broadcast accepted by one endpoint” as the start of monitoring rather than the finish line. Mempools can differ, propagation can be slow, and fee choices can change the time-to-confirm dramatically.

Limitations and edge cases you’ll meet in production

A Bitcoin wallet integration rarely fails because elliptic curves are hard. It fails because small operational details add up. You can design around most of them if you name them early and keep them visible during testing.

UTXO set drift and mempool surprises

Your balance view depends on the UTXO truth. If you rely on a third-party API, you inherit its indexing delays. If you rely on Electrum, you inherit server behavior and potential partial outages. If you run your own indexer, you inherit reorg handling and data consistency across restarts.

This becomes a UX issue. A user sees a confirmed balance, spends it, and then sees it “return” because your data source temporarily missed the mempool spend. Treat mempool awareness as part of the product contract, even if you start with a simplified model.

Fee estimation under congestion

Fee estimation is not a one-size calculation. It depends on mempool pressure and confirmation targets. If you hard-code fees during testing, you’ll eventually ship a wallet that creates transactions that confirm slowly under load.

A healthy integration treats fee estimation as a strategy with inputs and outputs. You can swap the estimator later without rewriting signing code, as long as your pipeline keeps “amounts in sats” as exact integers.

PSBT input data requirements

This is the part that turns “signing” into an integration exercise. Different script types require different data in PSBT inputs. If you omit required fields, the transaction may fail to finalize or may be rejected by the network after broadcast.

PSBT exists to standardize the data required for signing across systems, which is exactly why BIP-174 and related PSBT topics are worth reading early rather than after the first “cannot sign input” error.

Browser and mobile build constraints

bitcoinjs-lib aims to work in Node.js and browsers, and its README discusses environment considerations such as RNG sources and Buffer behavior.

Modern builds can still hit friction around WebAssembly dependencies or crypto primitives, especially in mobile webviews or React Native environments. If you plan to ship on mobile, test the full signing pipeline in that exact runtime early. Bitcoin signing issues that show up only on-device are slow to debug because they often involve low-level byte handling and subtle polyfill behavior.

Change outputs and “address reuse” bugs sneak in through state handling

Even with HD derivation in place, production wallets still stumble into accidental address reuse because the app doesn’t persist derivation indexes and gap-limit state reliably. A reinstall, a device migration, or a partial restore can reset the “next receive address” counter and make the wallet hand out an old address again. That creates two problems at once: privacy degrades because multiple deposits cluster on-chain, and UTXO discovery becomes harder because your scanning logic now has to reconcile activity across reused addresses and potentially multiple “current” branches. 

The fix is mostly operational discipline: persist your last-used receive and change indexes, store enough metadata to rebuild address windows deterministically, and ensure restore flows rebuild that state from chain data rather than from UI assumptions.

Conclusion

bitcoinjs-lib is a pragmatic way to build Bitcoin wallet functionality in JavaScript, yet it works best when you treat “wallet integration” as a pipeline rather than a snippet. BIP-39 and BIP-32 define how you get from mnemonic to key tree. BIP-84 gives you a well-understood path for native SegWit address derivation. PSBT provides a structured container for constructing and signing transactions across components, with BIP-174 as the core reference.

Once keys, derivation, UTXO sync, PSBT signing, and broadcast each have a clear contract, the wallet stops feeling fragile. That’s the point where “integrate a Bitcoin wallet” becomes repeatable engineering work instead of a string of one-off fixes.