Documentation Index
Fetch the complete documentation index at: https://docs.moralis.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Bitcoin Streams extends Moralis Streams to the Bitcoin network, delivering real-time transaction data via webhooks. Moralis indexes Bitcoin automatically and pushes structured payloads to your endpoint whenever transactions match your criteria.
Webhooks typically arrive within seconds of a block being mined — no node, no polling, no missed blocks.
Key Features
- Real-time delivery: end-to-end latency is typically under 10 seconds after a block is mined
- Mempool notifications: unconfirmed transactions fire a webhook the moment they hit the mempool — react before the next block is mined
- Three-phase notifications: matches can fire up to three times — first when the transaction is broadcast to the mempool (
block.hash: "mempool"), again when it lands in a block (confirmed: false, near the chain tip), and finally after the 2-block confirmation depth (confirmed: true, reorg-safe)
- All address formats supported: P2PKH (legacy,
1...), P2SH (SegWit-wrapped, 3...), and Bech32 native SegWit (bc1q...)
- Xpub support: attach an extended public key to a stream and Moralis derives and monitors its addresses automatically — ideal for HD wallets
- Flexible monitoring: watch specific addresses, attach an xpub, or enable firehose mode with
allAddresses to receive every Bitcoin transaction
- Automatic retries: Moralis retries webhook delivery until it receives an HTTP 200 response
How It Works
- A transaction is broadcast to the Bitcoin mempool. Moralis evaluates every mempool transaction against your registered addresses and xpubs
- Matching mempool transactions fire a webhook with
confirmed: false and the sentinel values block.height: "0" / block.hash: "mempool" — delivered once per stream
- A new Bitcoin block is produced and Moralis evaluates every transaction in it against your registered addresses
- Matching block transactions fire a webhook with
confirmed: false and the real block height / hash — the block is close to the chain tip and could still be reorged
- After 2 additional blocks have been mined on top, Moralis re-fetches the block and delivers a third webhook with
confirmed: true — the transaction is now reorg-safe
- Your service upserts on
txid to reconcile the lifecycle, branching on block.hash === "mempool" for the mempool stage
Webhook Payload Structure
Each Bitcoin webhook payload contains:
- Block metadata — hash, height, timestamp, difficulty, merkle root, transaction count
- Transaction details —
txid, input/output counts, locktime, version, block reference
- Outputs (
vout) — value in BTC (not satoshis), script type, recipient address
- Inputs (
vin) — structural fields only; address and value fields remain null
- Confirmation status —
confirmed: false for the near-tip delivery, confirmed: true once the block has cleared the 2-block confirmation depth
Output values are represented as BTC decimals (e.g. 3.13258866). To convert to satoshis, multiply by 1e8:const satoshis = Math.round(btc * 1e8);
Mempool Notifications
Bitcoin Streams emit webhook notifications for unconfirmed transactions as soon as they hit the mempool — no need to wait for block inclusion. The payload shape is identical to a confirmed-block webhook, with sentinel fields on the block object marking it as mempool:
| Field | Mempool value | Confirmed-block value |
|---|
block.height | "0" | real block height |
block.hash | "mempool" | real block hash |
confirmed | false | false then true |
Everything else (transactions[], vin, vout, addresses, value, fee) is populated normally.
Example payload
{
"streamId": "cefa6529-e3ec-4ee8-8cbd-206d2e7ce64e",
"chainId": "btc-mainnet",
"confirmed": false,
"block": {
"height": "0",
"hash": "mempool"
},
"retries": 0,
"transactions": [ ]
}
Detecting mempool in your handler
Branch on either sentinel field — block.hash is the most explicit:
const isMempool = payload.block.hash === "mempool";
Behavior
- Delivered at most once per stream per matching mempool transaction
- No follow-up webhook on confirmation from the mempool delivery itself — the regular confirmed-block stream handles
confirmed: false (in-block) and confirmed: true (reorg-safe) for the same txid
- Mempool transactions are not guaranteed to confirm. They can be evicted, replaced (RBF), or expire on low fee. Treat them as pending, not settled
- Matching covers both sends and receives. In mempool payloads,
vin and vout addresses are both populated, so a watched address triggers a match whether it is sending or receiving
- Xpub-derived addresses are fully supported — addresses derived from an attached xpub match against the mempool the same way they do against confirmed blocks
Getting started
No new endpoint, parameter, or schema. Existing Bitcoin streams pick up mempool notifications automatically. Just branch on block.hash === "mempool" (or block.height === "0") in your handler to separate mempool events from confirmed-block events.
Configuration Parameters
When creating a Bitcoin stream you specify:
| Parameter | Description |
|---|
webhookUrl | Public HTTPS endpoint that will receive payloads (required) |
network | Must be ["mainnet"] as an array |
includeInputs | Include input structure in the payload |
includeOutputs | Include output structure in the payload |
allAddresses | Firehose mode — deliver every Bitcoin transaction |
description | Human-readable stream description |
tag | Identifier for routing / filtering on your side |
API Endpoints
All Bitcoin Streams endpoints require the x-api-key header.
Streams
| Method | Endpoint | Purpose |
|---|
PUT | /streams/bitcoin | Create a stream with a webhook URL and configuration |
POST | /streams/bitcoin/{id} | Update a stream’s configuration |
POST | /streams/bitcoin/{id}/status | Pause or resume a stream |
GET | /streams/bitcoin | List existing Bitcoin streams |
GET | /streams/bitcoin/{id} | Get a stream by ID |
DELETE | /streams/bitcoin/{id} | Remove a stream |
POST | /streams/bitcoin/{chainId}/block/{blockNumber} | Get webhook data for a historical block |
POST | /streams/bitcoin/{chainId}/block-to-webhook/{blockNumber}/{streamId} | Replay a historical block for backfilling |
Addresses
| Method | Endpoint | Purpose |
|---|
GET | /streams/bitcoin/{id}/address | List addresses attached to a stream |
POST | /streams/bitcoin/{id}/address | Add an address to monitor (one per request) |
DELETE | /streams/bitcoin/{id}/address | Remove an address from a stream |
Xpub
| Method | Endpoint | Purpose |
|---|
GET | /streams/bitcoin/{id}/xpub | List xpubs attached to a stream |
POST | /streams/bitcoin/{id}/xpub | Add an xpub to derive and monitor addresses from |
DELETE | /streams/bitcoin/{id}/xpub/{xpubId} | Remove an xpub from a stream |
Limitations
Bitcoin Streams has a few behaviors that differ from EVM Streams — plan your consumer around them.
- Input data is not populated. Even with
includeInputs: true, input address and value fields return null. You can reliably detect inbound transfers to watched addresses, not outflows.
isCoinbase is always false. The isCoinbase field does not reliably identify coinbase transactions. Detect coinbase transactions using a heuristic: the transaction contains an OP_RETURN output and has exactly one input.
- Values are BTC decimals. Output values are not denominated in satoshis. Convert with
Math.round(btc * 1e8) before feeding integer-based ledger systems.
- Mainnet only. The
network parameter must be ["mainnet"].
Processing Patterns
Branching on mempool vs. confirmed block
A single txid can arrive up to three times: once from the mempool, once on block inclusion, and once on reorg-safe confirmation. Branch on the block.hash sentinel before doing any block-specific work:
if (payload.block.hash === "mempool") {
// pending: tx broadcast but not yet mined
} else if (payload.confirmed === false) {
// included in a block near the chain tip — could still reorg
} else {
// reorg-safe — safe to persist
}
Deduplication
Use the transaction id (txid) as your natural deduplication key. Upsert on each delivery, advancing your state machine through mempool → in-block → confirmed as each webhook arrives. Note that the mempool delivery can be skipped entirely for transactions Moralis only observes once they are already mined.
Coinbase detection
Since isCoinbase is unreliable, detect coinbase transactions like this:
const isCoinbase =
tx.vin.length === 1 &&
tx.vout.some((out) => out.scriptPubKey?.type === "nulldata"); // OP_RETURN
Value conversion
Store both BTC and satoshi values so downstream services can pick whichever representation fits:
const btc = vout.value;
const satoshis = Math.round(btc * 1e8);
Common Use Cases
- Exchange deposits — surface deposits the moment they hit the mempool (
block.hash === "mempool"), mark them pending on block inclusion (confirmed: false), and confirmed on the reorg-safe delivery (confirmed: true)
- Pending balances — show users a pending balance the instant a tx is broadcast, before any block has been mined
- Payment UX — trigger downstream flows on payment broadcast instead of waiting for confirmation
- Mempool analytics — RBF detection, fee analytics, and MEV / arbitrage signals against unconfirmed activity
- Cold wallet surveillance — alert on any inbound transfer to an offline storage address
- Mining pool revenue — watch payout addresses for both standard payments and coinbase block rewards
- Treasury monitoring — real-time accounting for corporate or DAO-held BTC addresses
Setup Checklist
- Generate an API key from admin.moralis.com
- Deploy a publicly accessible HTTPS webhook endpoint
- Create the stream via
PUT /streams/bitcoin
- Attach the wallets you want to monitor — either by registering individual addresses (
POST /streams/bitcoin/{id}/address) or by adding an xpub (POST /streams/bitcoin/{id}/xpub) to auto-derive and watch its addresses
- Implement the verification handler — respond to the empty-body test
POST with HTTP 200
- Build upsert logic keyed on
txid to handle the confirmed: false → confirmed: true transition
- Always return HTTP
200 from your handler, even when your own processing errors — Moralis retries on non-200 responses