BitcoinBTCCustodianExchangeDeposit DetectionUTXOGraphQL API

How to Detect Bitcoin Deposits Without Running a Full Node

The architecture custodial Bitcoin products use to detect customer deposits and withdrawals without operating bitcoind plus an indexer.

Cover Image for How to Detect Bitcoin Deposits Without Running a Full Node

A custodial Bitcoin product has the same data problem every other UTXO custodian has. The product needs to detect inbound deposits and outbound withdrawals against thousands of customer addresses, attribute every event to the right customer account, and stay in sync with the chain without operating bitcoind and an indexer fleet.

This article walks through the deposit reconciliation architecture used by production Bitcoin custodians on Bitquery. It covers the Bitcoin inputs and outputs query shape, the tip of chain heartbeat that drives the walker, the address-corpus batching pattern, the script-type filter for P2PKH versus SegWit versus P2SH wallets, the address book reconciliation step, the source-of-funds screening pass through the Bitquery MCP server, the finality buffer for safe deposit confirmation, and the portability of the same skeleton to Litecoin and Dogecoin.

It is the Bitcoin companion to the Cardano deposits guide. Same three-component architecture, different chain, different UTXO model details.

The Three Component Architecture

Every production Bitcoin deposit detector reduces to three components.

The first component is a tip of chain heartbeat. It fires on a fixed schedule and returns the current Bitcoin block height. This tells the rest of the pipeline how far the chain has advanced since the last sweep.

The second component is an address-corpus walker. It calls the Bitcoin inputs and outputs queries with height: {gteq: X, lteq: Y} filters and a batch of customer addresses, then pages through the results. Each iteration extends the walked range by a fixed chunk size until the cursor catches up to tip.

The third component is a reconciler. It joins the walker's output against the custodian's internal address book, attributes each transaction to a customer account, applies the confirmation buffer, and writes a deposit or withdrawal event to the ledger.

The three components run independently. The heartbeat decides when there is new work. The walker does the work. The reconciler turns the work into business state. Failures in one do not cascade into the others.

Tip of Chain Heartbeat

The heartbeat is one query that returns the current block height. Open the Bitcoin tip of chain query in the Bitquery GraphQL IDE to test it.

{
  bitcoin(network: bitcoin) {
    blocks(options: {limit: 1, desc: "height"}) {
      height
      timestamp {
        iso8601
      }
    }
  }
}

This is the cheapest query in the pipeline. Cache the response per second on your own side if the heartbeat fires more often than that. Bitcoin produces a block roughly every ten minutes, so a higher cadence buys nothing for freshness.

The heartbeat does two jobs. It tells the walker the upper bound of the next chunk to fetch. It also signals chain liveness, so a heartbeat that fails repeatedly is the earliest indicator that something upstream is wrong.

Address Corpus Walker

The walker is the workhorse query. It fetches every input and every output touching a batch of customer addresses inside a bounded block range. Open the Bitcoin deposit walker block range query in the Bitquery GraphQL IDE to run it.

{
  bitcoin(network: bitcoin) {
    inputs(
      inputAddress: {in: [
        "bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h",
        "34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo",
        "bc1qgdjqv0av3q56jvd82tkdjpy7gdp9ut8tlqmgrpmv24sq90ecnvqqjwvw97"
      ]}
      height: {gteq: 950718, lteq: 951218}
      options: {asc: "block.height", limit: 1000, offset: 0}
    ) {
      block {
        height
        timestamp {
          iso8601
        }
      }
      transaction {
        hash
      }
      inputIndex
      inputAddress {
        address
      }
      outputTransaction {
        hash
      }
      value
      inputScriptType {
        type
      }
    }
    outputs(
      outputAddress: {in: [
        "bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h",
        "34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo",
        "bc1qgdjqv0av3q56jvd82tkdjpy7gdp9ut8tlqmgrpmv24sq90ecnvqqjwvw97"
      ]}
      height: {gteq: 950718, lteq: 951218}
      options: {asc: "block.height", limit: 1000, offset: 0}
    ) {
      block {
        height
        timestamp {
          iso8601
        }
      }
      transaction {
        hash
      }
      outputIndex
      outputAddress {
        address
      }
      value
      outputScriptType {
        type
      }
    }
  }
}

The inputAddress: {in: [...]} and outputAddress: {in: [...]} filters take a batch of customer addresses, so one call covers many users. Smaller batches mean more requests. Larger batches risk hitting timeouts on busy block ranges or on wallets with high activity.

The height: {gteq: X, lteq: Y} filter bounds the work to a specific block range, which makes the call deterministic and idempotent. Re-running the same (addresses, height range) returns the same result every time. The reconciler can safely retry a chunk.

The asc: "block.height" ordering guarantees the walker sees events in chain order. Deposits and the spends that consume them arrive in the same order they appear on chain, which simplifies the reconciler's deduplication logic.

Script Type Filtering for P2PKH, SegWit, and P2SH

Bitcoin addresses come in three common formats, each tied to a different output script type. A custodian's address book often holds all three because legacy addresses are still in circulation, some inbound flows originate from older exchanges, and modern wallets default to SegWit.

The outputScriptType: {is: "pubkeyhash"} filter restricts the walker to P2PKH outputs (legacy addresses starting with 1). This is the right filter for an address corpus generated under the older derivation paths. Open the script-type filtering query in the Bitquery GraphQL IDE to run it.

{
  bitcoin(network: bitcoin) {
    outputs(
      outputAddress: {in: ["1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF"]}
      height: {gteq: 950718, lteq: 951218}
      outputScriptType: {is: "pubkeyhash"}
      options: {asc: "block.height", limit: 1000}
    ) {
      block { height }
      transaction { hash }
      outputIndex
      value
      outputScriptType { type }
    }
  }
}

For SegWit native addresses (bech32, starting with bc1q for P2WPKH or bc1p for P2TR Taproot), use outputScriptType: {is: "witness_v0_keyhash"} for P2WPKH or outputScriptType: {is: "witness_v1_taproot"} for Taproot. Dropping the filter altogether returns matching outputs regardless of script type, which works when the address corpus is a strict whitelist. For mixed corpora, run separate walker queries per script type and merge the results in the reconciler. This avoids the case where a script-type filter accidentally excludes a deposit the customer actually received.

For P2SH wallets (addresses starting with 3), use outputScriptType: {is: "scripthash"}. The same pattern works.

A custodian managing all three script types runs three walker queries per chunk, one per type, and writes the union to the reconciler. The total work is similar to running one unfiltered query, with the bonus that each script-type stream can be monitored separately.

To see what production-scale activity looks like at one address, point the walker at a real Bitcoin hot wallet over a recent 500-block window. The Binance wallet at bc1qm34lsc65zpw79lxes69zkqmk6ee3ewf0j77s3h received 3,016 outputs totaling 53,071.48 BTC between block 950,718 and 951,218 (roughly three and a half days at the time of writing). Every one of those outputs returned outputScriptType.type of witness_v0_keyhash. A custodian polling a corpus of such wallets has to handle thousands of events per address per day at peak.

Chunk Size, Steady State, and Catch Up

The block range chunk size is the main lever for throughput. Bitcoin's block interval averages ten minutes, so a 100-block chunk covers roughly 17 hours of chain history and a 1,000-block chunk covers about a week. The right number depends on how active the address corpus is and how aggressive the timeout limits are.

Steady state runs at tip. The walker watches the heartbeat, and when a new block lands it fetches the chunk from lastWalkedHeight + 1 to tipHeight. Most of the time this chunk is small (one to a few blocks) and resolves in well under a second.

Catch up mode runs when the walker is more than one chunk behind tip. This happens after deployment, after a service restart that lost the cursor, or after extended downtime. The walker fetches chunk after chunk in sequence until the cursor reaches tip, at which point it falls back to steady state.

The catch-up rate on Bitquery is bounded by query throughput. A custodian backfilling six months of history (roughly 26,000 Bitcoin blocks) can typically catch up in minutes to an hour depending on address corpus size. Syncing bitcoind from genesis against current chain size is a multi-day operation that occupies a full machine.

Address Book Reconciliation

The walker returns chain level data. Each output carries the recipient address, the transaction hash, the output index, and the value. The chain has no concept of which customer owns that address. That mapping lives inside the custodian's own systems. The reconciler is the component that joins the two: it takes each output the walker found and looks the recipient address up against the custodian's internal address book to attribute the deposit to a customer.

The reconciler holds two tables. The first is the address book, mapping every customer to the deposit addresses generated for them. The second is the ledger, the custodian's source of truth for deposits and withdrawals.

For every output returned by the walker, the reconciler looks up the recipient address in the address book, attributes the value to a customer, and writes a deposit event to the ledger keyed on the (transaction.hash, outputIndex) pair. For every input, it does the same, this time as a withdrawal event keyed on the (transaction.hash, inputIndex) pair.

The transaction hash plus index pair is the natural idempotency key. The walker is allowed to re-run any chunk safely because the reconciler will deduplicate any event it has already seen.

A practical scaling note. The address book lookup is the hot path. Keep it in memory or in a low latency key value store. A custodian with 50,000 addresses needs sub millisecond lookups per output, not 10 millisecond round trips to a relational database.

Source of Funds Screening with the Bitquery MCP Server

A regulated custodian usually has a second job after attribution. Before the deposit clears to the customer's available balance, the back office wants to know where the funds came from. Did the sending wallet belong to a sanctioned entity? Did the coins pass through a mixer a few hops back? Is the source a darknet market?

The walker answers "a deposit happened." The Bitquery MCP server answers "where did it come from." It exposes Bitcoin tracing and Bitquery's address labels as tools that an AI client, such as Claude or ChatGPT, or a custom agent built on the MCP SDKs can call. A practical split: the reconciler holds any deposit above a set size, or from a first-time sender, in a review queue, and the compliance analyst or agent screens it through the MCP server before it clears.

The screen runs in three steps, each a single tool call.

StepToolWhat it returns
1. List the sendersbtc_tx_flow on the deposit's transaction hashEvery input address with its amount, and which outputs are change
2. Check the senderslabels_for_addresses on those input addressesSanctions, mixer, darknet, scam and exchange labels, with the source of each
3. Go one hop backbtc_tx_flow on each input's Funded_By_Tx, then labels againThe wallets that paid the sender, for a deeper provenance check

Here is the screen run on a real payment. On 18 February 2020, transaction D116B2DC…100B7F paid 140.14 BTC to 392XxH946Q1BNcDuBtD3ZNdRF7vCKpnqYv. Step 1 shows five input addresses, which together put in 159 BTC. Step 2 finds one of them, 123WBUDmSJv4GctdVEz6Qq6z8nXSKrJ4KX, on the US Treasury's sanctions list as a wallet of the Hydra darknet market; the other four carry no label. Addresses spent together in one transaction usually share an owner, so one labelled input is enough to hold the deposit. The MCP tool btc_related_addresses lists the rest of that wallet's addresses.

The date matters. OFAC sanctioned Hydra on 5 April 2022, two years after this payment. A screen run on the day of the deposit would have passed it. That is why the screening record should store the labels and their sources as they stood when the deposit cleared, and why held and recent deposits are worth screening again when a sanctions list changes.

In an AI client, the same screen is one request:

Bitcoin transaction D116B2DCBAFC3C10CC4F55014108E79797D7DA45EEBCBC5E60764BD768100B7F
paid one of our customers. List its input addresses, label every one,
and follow any unlabelled input one step back. Flag sanctions,
mixer or darknet labels.

The screening pass is per deposit, not per chunk. The walker batches addresses for throughput; the screen needs its own provenance trail for each deposit. Cap concurrent screens and queue the rest behind the confirmation buffer, which gives at least six blocks, about an hour, to finish before the deposit would clear anyway.

Persist the screening result alongside the deposit event in the ledger. Auditors will ask what the screen said at the moment the deposit cleared. Storing the label set returned for each address, or a hash of it, on the ledger row answers that question six months later. Tracing and labels sit on the AI Investigation MCP plan, at $149 per month on our pricing page.

For a one-off manual check on a flagged deposit, MoneyFlow Lite follows funds from an address in your browser, on 17 chains including Bitcoin. It draws on your Bitquery API quota; try it on the 7-day trial.

The Six Confirmation Buffer

Bitcoin transactions are not final the moment they appear in a block. A reorg can invalidate a recently mined block and roll back any deposit credited inside it. The standard mitigation is to delay deposit confirmation by a fixed number of blocks, conventionally six on Bitcoin (roughly an hour of wall clock time).

The walker can still fetch every block at tip and feed the reconciler. The reconciler holds events in a pending state until they clear the buffer, then promotes them to confirmed. The number of confirmations is a policy choice. Six is the long-standing community default for full settlement. Lower thresholds trade reorg risk for faster credit and are reserved for low-value flows or trusted counterparties.

The same buffer applies to withdrawals. A withdrawal that lands in a reorg loses its block but the transaction itself still exists in the mempool. The reconciler should treat unconfirmed withdrawals as in-flight, not as completed.

Bitquery vs Self Hosted bitcoind plus Indexer

The natural alternative is running bitcoind against an indexer such as Electrs, BTC RPC Explorer, or Esplora, then querying that indexer over RPC or HTTP. This is a legitimate choice for some teams. It is the wrong choice for most custodians.

DimensionSelf hosted bitcoind + indexerBitquery GraphQL
Time to first deposit detectedMulti day initial node sync (700 GB+)Minutes (sign up, key, query)
Operating componentsbitcoind + indexer + storage layerOne HTTP endpoint
Disk requirements700 GB and growing for a pruned-and-indexed setupNone
Catch up from historyRe sync from genesis (days)Walk block ranges (minutes)
Address batch filterBuild your own filter on top of raw chain dataBuilt in via inputAddress: {in: [...]}
Script type decodingParse output scripts yourselfBuilt in via outputScriptType
High availabilityCustomer builds and operatesBuilt in
Multi UTXO chain coverageSeparate node fleet per chain (BTC, LTC, DOGE, BCH)Same API, different network argument

The case for bitcoind plus a custom indexer is sub-50-millisecond latency on private queries, full control over the storage schema, and very large historical scans (multi-year, full chain). The case against is everything else.

Operationally, running a Bitcoin node fleet for deposit detection means paying the cost of every Bitcoin Core release in resync time and migration work. Node downtime stops the deposit feed cold. None of these failure modes exist on Bitquery's hosted layer.

Multi UTXO Portability: The Same Loop on Litecoin and Dogecoin

The whole reconciliation pattern above ports to other UTXO chains without architectural changes. Swap network: bitcoin for network: litecoin or network: dogecoin, swap the address format in the corpus, and the same heartbeat plus walker plus reconciler runs against the new chain.

{
  bitcoin(network: litecoin) {
    outputs(
      outputAddress: {in: ["ltc1qyu50pytvc08yd5f2ycy4fd58nlmuhnwt4wwmyn"]}
      height: {gteq: 3113000, lteq: 3114000}
      options: {asc: "block.height", limit: 1000}
    ) {
      block { height }
      transaction { hash }
      value
      outputAddress { address }
    }
  }
}
{
  bitcoin(network: dogecoin) {
    outputs(
      outputAddress: {in: ["9wYP6KhQm8vwxgHsBn8GE62u7hmHwKm1oz"]}
      height: {gteq: 6220500, lteq: 6222000}
      options: {asc: "block.height", limit: 1000}
    ) {
      block { height }
      transaction { hash }
      value
      outputAddress { address }
    }
  }
}

Bitcoin Cash works the same way under network: bitcash. A custodian supporting BTC, LTC, DOGE, and BCH runs four instances of the loop, one per chain, against the same underlying Bitquery API. The reconciler shares the ledger schema across all chains. A new UTXO chain integration is a network-argument change, not a new infrastructure project.

The cross-chain numbers from the example windows above tell the story. The Bitcoin walker over a 500-block window (about three and a half days) returned 2,934 inputs and 3,016 outputs across the three Binance addresses, totaling 50,651 BTC sent and 53,071 BTC received. The Litecoin walker over a 1,000-block window (around a day and a half) returned 2,663 outputs totaling 52.5 million LTC at the single recipient address. The Dogecoin walker over a 1,500-block window returned 14 outputs totaling 3.89 billion DOGE at a P2SH cold-storage address, which is the canonical exchange-cold-storage signature: low event count, very high per-event value. Same loop. Same query shape. Different chain, different traffic profile.

Putting it Together

The full deposit detector loop is short.

every 30 to 60 seconds:
    tipHeight = heartbeat()

    if tipHeight - lastWalkedHeight > chunkSize:
        offset = 0
        while chunk not exhausted:
            results = walker(addressBatch, nextChunk, limit, offset)
            for each input, output in results:
                reconciler.apply(result, key=(transaction.hash, inputIndex or outputIndex))
            offset += limit

        # only advance the cursor once the reconciler has persisted the chunk
        if reconciler.confirmed(chunk):
            lastWalkedHeight = chunkEndHeight

    reconciler.promote_buffered_events(currentConfirmations=tipHeight - eventHeight)

The custodian's deposit and withdrawal feeds are now driven by Bitcoin chain state with no node fleet to operate.

This is the same architecture used by production Bitcoin custodians on Bitquery. It pairs with the Cardano deposits guide and the Avalanche balance polling guide to give a complete custodial data layer across UTXO and EVM chains.

Summary

A custodial Bitcoin product detecting deposits and withdrawals at scale has three real choices. Build and operate a Bitcoin node fleet and a custom indexer. Stream from a managed RPC provider and reconstruct UTXO movements per call. Or run a tip heartbeat plus an address-corpus walker against Bitquery and let the reconciler handle attribution.

The third option exists specifically because the deposit detection pattern is so universal across UTXO custodians, exchanges, and multi-chain wallets. The queries in this article are the canonical query shapes these products converge on. The same skeleton runs unchanged against Litecoin and Dogecoin.

Related Resources

Updated on 16 October 2026: the Coinpath screening queries were replaced with a source-of-funds screen through the Bitquery MCP server, run on a real 2020 payment. AI tools helped with the edit. Bitquery sells the APIs and the MCP server described here.

Subscribe to our newsletter

Subscribe and never miss any updates related to our APIs, new developments & latest news etc. Our newsletter is sent once a week on Monday.