
Reading on-chain data from JavaScript has always been the awkward half of the job. Pulling a clean Bitcoin balance โ confirmed plus unconfirmed, the way an explorer shows it โ usually means running your own indexer or hammering a flaky public API. Listing the NFTs and tokens an Ethereum or EVM address holds means stitching together three or four explorer endpoints, each with its own schema, rate limit and missing fields. And once you actually want to broadcast a transaction, you are back to bitcoinjs-lib for Bitcoin, ethers or viem on the EVM side, and a half-maintained fork for Dogecoin and Litecoin. ChainGate covers all of it through a single SDK.
ChainGate folds that work into a single JavaScript SDK. Built as a multi-chain wallet SDK, the same call shape sends Bitcoin, Ethereum, Avalanche (and every EVM chain), Litecoin, Dogecoin and Bitcoin Cash, reads address balances and lists the assets sitting at any address. The same package runs in Node.js and the browser without code changes โ every crypto primitive ships natively, with no polyfills required.
Here is the basic five-line send. The rest of this guide walks the same pattern across every chain, then layers in fee estimation and balance checks before broadcasting:
import { ChainGate, importWallet } from 'chaingate';
const cg = new ChainGate({ apiKey: 'your-api-key' });
const wallet = importWallet({ phrase: process.env.MNEMONIC });
const btc = cg.connect(cg.networks.bitcoin, wallet);
const tx = await btc.transfer(cg.networks.bitcoin.amount('0.001'), 'bc1qRecipient...');
await tx.signAndBroadcast();The short version
- One SDK, every chain. The same transfer() API covers Bitcoin, Ethereum, Avalanche, Litecoin, Dogecoin, Bitcoin Cash and every EVM-compatible network.
- Runs unchanged on Node.js 22+ and modern browsers โ no Buffer or crypto polyfill setup required.
- Fees, balance and broadcast are all built in โ no second library needed for any of them.
- Signing always happens locally. Your private key never leaves the SDK boundary.
1. Setup: install and initialize ChainGate
Pick your package manager. The SDK ships as a single npm package โ install it once and you can send Bitcoin, Ethereum and the rest from the same import, types included:
# pick your package manager
npm install chaingate
pnpm add chaingate
yarn add chaingateThen grab a free API key from the ChainGate dashboard. Sign-up takes about a minute and the free tier is enough to ship a prototype to production. Initialize the client with that key once and reuse it for every chain:
import { ChainGate } from 'chaingate';
const cg = new ChainGate({ apiKey: process.env.CHAINGATE_API_KEY });Same code in Node.js and the browser. The package works on Node 22+, modern browsers (Vite, Next.js, Webpack 5+) and Bun out of the box, so the same Node.js blockchain library you import on the server is the one that runs in the user's browser. Crypto primitives like Buffer and randomBytes ship natively inside the SDK โ nothing to polyfill, nothing to configure.
TypeScript types are bundled. There is no @types/chaingate package because there is no need for one โ every public class, method and event is fully typed at the source.
2. Sending Bitcoin, Litecoin, Dogecoin and Bitcoin Cash
Bitcoin, Litecoin, Dogecoin and Bitcoin Cash all share the same accounting model โ UTXOs. A wallet's balance is the sum of unspent transaction outputs locked to its addresses. To send funds, the wallet picks enough UTXOs to cover the amount, builds a new transaction that consumes them as inputs, sends the recipient one output, sends any leftover back to itself as a change output, and pays a fee priced in satoshis per kilobyte (or the per-chain equivalent).
ChainGate handles every step of that โ UTXO selection, change output, fee tier and signing โ behind a single transfer() call. You give it a destination address and an amount, you get back a signed transaction ready to broadcast.
Send Bitcoin with Node.js or in the browser
The base example. Connect your wallet to the Bitcoin network, create the transfer, choose a fee tier and broadcast:
import { ChainGate, importWallet, NotEnoughFundsError } from 'chaingate';
const cg = new ChainGate({ apiKey: process.env.CHAINGATE_API_KEY });
const wallet = importWallet({ phrase: process.env.MNEMONIC });
const btc = cg.connect(cg.networks.bitcoin, wallet);
const amount = cg.networks.bitcoin.amount('0.001'); // 0.001 BTC
try {
const tx = await btc.transfer(amount, 'bc1qRecipient...');
tx.setFee(tx.recommendedFees().normal);
const broadcast = await tx.signAndBroadcast();
console.log('BTC txid:', broadcast.transactionId);
} catch (err) {
if (err instanceof NotEnoughFundsError) {
console.error('Not enough BTC for this transfer + fee');
}
throw err;
}To target testnet during development, swap cg.networks.bitcoin for cg.networks.bitcoinTestnet โ the rest of the code stays the same.
Send Litecoin with Node.js or in the browser
Same code, different network. The diff against the Bitcoin snippet is literally one identifier:
const ltc = cg.connect(cg.networks.litecoin, wallet);
const amount = cg.networks.litecoin.amount('0.1'); // 0.1 LTC
const tx = await ltc.transfer(amount, 'ltc1qRecipient...');
tx.setFee(tx.recommendedFees().normal);
await tx.signAndBroadcast();Litecoin uses ltc1qโฆ SegWit addresses by default and confirms blocks every two and a half minutes โ about four times faster than Bitcoin โ which makes it a popular choice for low-value payments where settlement speed matters more than security depth.
Send Dogecoin with Node.js or in the browser
Same diff, different chain:
const doge = cg.connect(cg.networks.dogecoin, wallet);
const amount = cg.networks.dogecoin.amount('100'); // 100 DOGE
const tx = await doge.transfer(amount, 'DRecipientAddress...');
tx.setFee(tx.recommendedFees().normal);
await tx.signAndBroadcast();Dogecoin's JavaScript ecosystem has lagged behind the rest of the UTXO family. Most popular bitcoinjs-lib forks have not been updated in years and the few maintained alternatives are private. ChainGate is one of the very few options shipping production-grade Dogecoin support in JavaScript today.
Send Bitcoin Cash with Node.js or in the browser
Bitcoin Cash uses CashAddr addresses by default but the SDK accepts the legacy format too โ you can paste either into transfer() and the network will still confirm:
const bch = cg.connect(cg.networks.bitcoincash, wallet);
const amount = cg.networks.bitcoincash.amount('0.05'); // 0.05 BCH
// CashAddr or legacy โ both work
const tx = await bch.transfer(amount, 'bitcoincash:qpRecipient...');
tx.setFee(tx.recommendedFees().normal);
await tx.signAndBroadcast();Address validation is bidirectional: cg.networks.bitcoincash.isValidAddress() returns true for both formats, so you do not have to normalise user input before sending.
Running UTXO sends in the browser
All four UTXO snippets above run unchanged in a Vite, Next.js or Webpack 5 bundle โ the same import works on the client side and the same wallet signs locally without contacting the API.
A note on safety: never load production private keys directly into a frontend. Either keep signing on the server and send only the broadcast call from the browser, or have the user supply the key through a wallet-style flow they control.
3. Sending Ethereum and EVM chains
Ethereum's account model is different โ there are no UTXOs, just an address with a balance and a monotonically increasing nonce. ChainGate hides that distinction behind the same transfer() call you used on Bitcoin. The differences (nonce management, EIP-1559 base fee + tip, gas limit) are computed for you:
const eth = cg.connect(cg.networks.ethereum, wallet);
const amount = cg.networks.ethereum.amount('0.01'); // 0.01 ETH
const tx = await eth.transfer(amount, '0xRecipient...');
tx.setFee(tx.recommendedFees().normal);
const broadcast = await tx.signAndBroadcast();
console.log('ETH txHash:', broadcast.transactionId);Send Avalanche with Node.js or in the browser
Avalanche's C-Chain is wired in as a first-class network alongside Ethereum โ same call shape, same wallet, AVAX-denominated amounts. The diff against the Ethereum snippet is one identifier:
const avax = cg.connect(cg.networks.avalanche, wallet);
const amount = cg.networks.avalanche.amount('1'); // 1 AVAX
const tx = await avax.transfer(amount, '0xRecipient...');
tx.setFee(tx.recommendedFees().normal);
await tx.signAndBroadcast();The C-Chain finalises blocks in roughly two seconds and runs the same EIP-1559 fee model as Ethereum, so AVAX transfers settle quickly and the recommendedFees() output is shaped identically. That makes Avalanche a popular fit for high-throughput payments where Ethereum gas would price the user out.
Same wallet, same SDK, every EVM chain. The hosted RPC URLs cover Polygon, Arbitrum, Base, BNB Chain and Sonic out of the box; for any other EVM network โ Optimism, Linea, Scroll, Gnosis, your own node, a testnet โ pass the JSON-RPC URL directly to evmRpc(). Sending POL on Polygon is a one-line diff:
const polygon = cg.networks.evmRpc({
rpcUrl: cg.rpcUrls.polygon,
chainId: 137,
name: 'Polygon',
symbol: 'POL',
});
const pol = cg.connect(polygon, wallet);
const amount = polygon.amount('5'); // 5 POL
const tx = await pol.transfer(amount, '0xRecipient...');
tx.setFee(tx.recommendedFees().normal);
await tx.signAndBroadcast();Nonce handling is automatic. The SDK reads the latest pending nonce from the chosen RPC before signing, so you do not have to track it yourself. Fee construction defaults to EIP-1559 (maxFeePerGas + maxPriorityFeePerGas) on chains that support it and falls back to legacy gas pricing on those that do not.
The chainId you pass to evmRpc() must be the one the RPC endpoint itself reports, or the SDK will sign for the wrong network. The EVM chain ID list has the number for Polygon, Arbitrum, Base, Optimism and 35 other networks and testnets, and shows how to read it straight from any RPC URL.
If the node answers a broadcast with "nonce too low", "replacement transaction underpriced" or "insufficient funds for gas * price + value", the guide to Ethereum transaction errors explains what each message means and which ones cost gas.
Browser support is identical to the UTXO chains. The same code is what you would put behind a 'Send' button in a dApp.
4. Estimating transaction fees before sending
Always show the fee before broadcasting. Users hate surprises, stuck transactions cost time, and a 'review and confirm' step turns a one-shot UI flow into something a user actually trusts.
ChainGate exposes the same recommendedFees() method on every chain โ it returns four tiers (low, normal, high, maximum) with the underlying numbers each tier needs. On Bitcoin you get a feePerKbSat rate and the estimated total in satoshis; on Ethereum and other EVM chains you get EIP-1559 maxFeePerGas and maxPriorityFeePerGas as bigint wei values plus an estimated confirmation window:
// Bitcoin โ fee rate in sat/kB, plus the total estimated fee in satoshis
const btcTx = await btc.transfer(cg.networks.bitcoin.amount('0.01'), 'bc1q...');
const btcFees = btcTx.recommendedFees();
console.table({
low: { satPerKb: btcFees.low.feePerKbSat, feeSat: btcFees.low.estimatedFeeSat, enough: btcFees.low.enoughFunds },
normal: { satPerKb: btcFees.normal.feePerKbSat, feeSat: btcFees.normal.estimatedFeeSat, enough: btcFees.normal.enoughFunds },
high: { satPerKb: btcFees.high.feePerKbSat, feeSat: btcFees.high.estimatedFeeSat, enough: btcFees.high.enoughFunds },
});
// Ethereum โ EIP-1559 numbers in gwei
const ethTx = await eth.transfer(cg.networks.ethereum.amount('0.1'), '0x...');
const ethFees = ethTx.recommendedFees();
const toGwei = (wei) => Number(wei) / 1e9;
console.table({
low: { gwei: toGwei(ethFees.low.maxFeePerGas), sec: ethFees.low.estimatedConfirmationSecs },
normal: { gwei: toGwei(ethFees.normal.maxFeePerGas), sec: ethFees.normal.estimatedConfirmationSecs },
high: { gwei: toGwei(ethFees.high.maxFeePerGas), sec: ethFees.high.estimatedConfirmationSecs },
maximum: { gwei: toGwei(ethFees.maximum.maxFeePerGas), sec: ethFees.maximum.estimatedConfirmationSecs },
});The pattern that works in production: build the transaction, call recommendedFees(), surface the four tiers to the user, let them pick, then call signAndBroadcast(). The transaction object stays in memory between estimating and broadcasting โ no rebuilding required.
5. Checking balance before sending
A balance check belongs at the very top of any send flow. It catches typos, prevents wasted gas on a transaction that will fail and gives the user a number they can sanity-check against. Every chain exposes the same one-liner โ pass an address (or use the connected wallet) and you get the confirmed balance and, on chains that support it, the unconfirmed split:
// Ethereum โ confirmed balance + live USD price
const eth = cg.connect(cg.networks.ethereum, wallet);
const ethBalance = await eth.addressBalance();
const ethUsd = await ethBalance.confirmed.toCurrency('usd');
console.log(`${ethBalance.confirmed.base()} ETH ($${ethUsd})`);
// Bitcoin โ confirmed and unconfirmed split
const btc = cg.connect(cg.networks.bitcoin, wallet);
const btcBalance = await btc.addressBalance();
console.log(`Confirmed: ${btcBalance.confirmed.base()} BTC`);
console.log(`Unconfirmed: ${btcBalance.unconfirmed.base()} BTC`);Combined with the fee estimate, the full pre-flight is three calls โ balance, fee, then send. Wire those together once and every send flow in your app gets the same pre-send validation for free, whether you check a crypto balance from Node.js on the server or from the browser.
6. ChainGate vs bitcoinjs-lib, ethers and other alternatives
ChainGate is not trying to replace every low-level library that came before it. Each of those tools has a clear job. Here is how the trade-offs line up when you need to send transactions across more than one chain:
| Capability | ChainGate | bitcoinjs-lib | ethers / viem | Custom per-chain |
|---|---|---|---|---|
| Supported chains | BTC, ETH, AVAX+EVM, LTC, DOGE, BCH | Bitcoin only | EVM only | 1 per library |
| Lines to send BTC | ~5 | ~40+ | n/a | varies |
| Browser-ready | Yes โ out of the box | Polyfills required | Yes | Varies |
| Built-in fee estimation | Yes | No | Partial | No |
| TypeScript types | First-class | Community | Yes | Varies |
Stick with bitcoinjs-lib or ethers when you need byte-level control over the transaction shape, or when your app talks to a single chain and bundle size is the deciding factor. Switch to ChainGate when you are juggling more than one chain, racing a deadline, or running a small team that cannot afford to specialise per blockchain.
7. Pricing and the free tier
The free tier is generous on purpose โ every plan, including the free one, ships with the full SDK: balances, asset listings, fee estimation, signing and broadcast across every chain we support. It is enough to ship a prototype, run a personal project or take an internal tool to production. There is no credit card on signup.
Paid tiers lift the rate limits and add priority support and burst capacity. Upgrades are a one-click flow inside the dashboard.
See the pricing page for the live numbers and the upgrade path.
8. Where to go from here
You have the model: one SDK, multiple chains, the same five-line shape on every one of them. From here, the documentation covers the rest of the surface area โ message signing, ERC-20 transfers, NFT moves, decoded transaction history. The examples on GitHub are a good starting point for end-to-end recipes you can copy. And the API dashboard is one click away when you are ready to grab a key and ship something. If you want to dig deeper into the mechanics behind a fee, the companion guide on Ethereum gas unpacks where the numbers in recommendedFees() actually come from. The guide to transaction hashes explains what the transactionId you get back from signAndBroadcast() looks like on each chain and how to check it on a block explorer.
Frequently asked questions
Does ChainGate hold my private keys?
Can I use ChainGate in the browser?
Yes โ every example in this guide runs unchanged in a Vite, Next.js or Webpack 5 bundle. The SDK bundles its own browser-compatible crypto, so there is nothing extra to install.
Which networks are supported?
ChainGate ships full-API support for Bitcoin, Bitcoin Testnet, Litecoin, Dogecoin, Bitcoin Cash, Ethereum and Avalanche, hosted JSON-RPC endpoints for Polygon, Arbitrum, Base, BNB Chain and Sonic, and bring-your-own-RPC support for every other EVM chain โ including Optimism, Linea, Scroll, Gnosis, testnets and self-hosted nodes. The full matrix is in the documentation.
Can I send testnet transactions?
Yes โ there is no special testnet flag. Pass the testnet network to the same cg.connect() call you use on mainnet: for Bitcoin, that is cg.networks.bitcoinTestnet; for an EVM testnet, pass the testnet's JSON-RPC URL and chain ID into cg.networks.evmRpc({ rpcUrl, chainId, name, symbol }). The rest of the code is identical to the mainnet examples in this guide.
What happens if a transaction fails?
The SDK surfaces a typed error you can catch and act on. Most pre-broadcast issues are recoverable โ the transaction object is rebuilt with a fresh nonce or a new UTXO selection on retry, so a second attempt usually goes through.
Is ChainGate open source?
The JavaScript SDK that signs and broadcasts on your machine is published on GitHub under the MIT license โ read the source, vendor it, fork it. The ChainGate API behind it is a managed service that handles everything else for you. Most teams use the open-source SDK with the hosted API, but you can point the SDK at any RPC URL you control if you would rather host your own infrastructure.
Why does ChainGate need an API key?
Build with ChainGate
Put these ideas to work โ ChainGate gives you wallets, transactions and managed RPC for Ethereum and 20+ other blockchains with one install.