Cover Image for How to Detect Stellar Deposits Without Running a Horizon Node

How to Detect Stellar Deposits Without Running a Horizon Node

Stellar
XLM
Custodian
Anchor
Deposit Detection
Payments
GraphQL API

A custodial Stellar product, anchor service, or multi-chain payments backend has the same data problem on Stellar that every other chain custodian has. The product needs to detect inbound deposits and outbound withdrawals against a corpus of user-facing Stellar addresses, attribute every event to the right account, decode native XLM and issued-asset payments alike, and stay in sync with the chain without operating stellar-core plus Horizon plus Postgres.

This article walks through the data layer Stellar custodians use on Bitquery to skip that node stack. It covers the Stellar transfer schema (what fields each record carries and why each one matters for production reconciliation), how the data is delivered at enterprise scale through Bitquery cloud data dumps, the API patterns the same data supports for smaller workloads and prototyping, and how the whole thing compares to building the same layer with a self-hosted Horizon instance.

It is the Stellar companion to the Bitcoin deposits guide and the Cardano deposits guide. Same overall framing of "use Bitquery as the data source, skip the node fleet." Stellar-specific schema, finality model, and asset semantics.

Enterprise Scale: Bitquery Cloud Data Dumps for Stellar

For enterprise and production-scale Stellar workloads, the recommended path is Bitquery Cloud Data Dumps. Bitquery delivers Stellar transfer data directly to a customer-controlled S3 bucket, Snowflake account, or Google Cloud destination. The customer connects their warehouse to the bucket, ingests the data, and runs reconciliation, screening, and downstream analytics against it. No stellar-core to operate, no Horizon indexer to maintain, no schema migrations on Stellar protocol versions.

The Stellar schema is documented as sample files in the Bitquery cloud-data-dump sample repository under the Stellar directory. The directory carries example data files and the S3 bucket link for end-to-end integration testing before signing up. A data team can pull a sample, validate the shape against their warehouse, and confirm the join keys before any commercial conversation.

This is the right path when any of the following is true: the deposit-detection workload runs against thousands of customer addresses, the warehouse needs full Stellar history (not just a recent ledger range), the volume justifies a dedicated cloud-data agreement, or the engineering team would rather connect a warehouse to a managed bucket than operate a Horizon stack. For most production custodial, anchor, and payments teams on Stellar, this is the default recommendation.

For smaller workloads, prototyping, or interactive exploration, the same data is queryable over the Bitquery GraphQL API. The query shapes covered later in this article are useful for getting familiar with the schema and for workloads where the volume does not yet justify a cloud-data agreement. Both interfaces serve the same underlying data layer.

The Stellar Transfer Schema: What Each Record Carries

A Stellar transfer record is only useful downstream if it carries enough fields to answer the warehouse's analytical questions. A row with only (timestamp, sender, receiver, amount) is enough for a simple ledger but does not cover Stellar's path payments, issued-asset metadata, operation-level granularity, or the per-operation idempotency key a reconciler needs. The Bitquery Stellar transfer schema goes deeper.

Each Stellar transfer record carries the following fields, taken straight from the sample file in the Stellar cloud-data-dump directory.

The identification fields carry hash (the parent transaction hash on Stellar, which uniquely identifies the transaction across the chain), operation_index (the position of this operation within the transaction, ranging from 0 to 99 because Stellar permits up to 100 operations per transaction), and operation_name (the operation type, for example payment, pathPayment, createAccount, or accountMerge). Together, (hash, operation_index) is the natural per-record primary key. Every reconciler keys its dedupe table on this pair.

The block context is captured by block (the Stellar ledger sequence number) and timestamp_iso8601 (the closed-ledger timestamp, ISO 8601, UTC). These let the warehouse partition the table by date, sort by ledger, and join transfers against other ledger-keyed data such as ledger-level metrics or fee statistics.

The counterparty fields carry sender_address and receiver_address (Stellar G-addresses, 56 characters, starting with G). For custodial deposit detection these two fields drive the address-book lookup. For anchor reconciliation they drive the customer attribution.

The value fields carry amountFrom, amountTo, and direction. direction is a categorical label on Stellar that distinguishes flow types (payment, pathPayment, and other operation flavors). amountFrom and amountTo differ on path payments where the sender pays in one asset and the receiver gets a different asset; on direct payments the two are equal.

The currency fields are where Stellar's multi-asset reality lives. Every transfer record carries a full currency_from_* block (asset symbol, name, issuer address, token type) and a full currency_to_* block. For a native XLM transfer both blocks read XLM / Lumen. For an issued-asset transfer (USDC, EURC, anchor-issued fiat tokens, project tokens) the issuer address populates and the symbol carries the asset code. For a path payment the two blocks differ, which is exactly the information the reconciler needs to book the credit and debit on the right ledger sides.

That gives thirteen fields per Stellar transfer record across four logical groups. A warehouse table populated from this schema can answer deposit detection, withdrawal reconciliation, path-payment booking, issued-asset flow analytics, anchor compliance reporting, and source-of-funds tracing from the same per-transfer fact table without a second data source.

Depth: Historical Range and Granularity

A Stellar transfer index is rarely useful unless it goes back far enough to answer the warehouse's analytical questions. Year-over-year USDC anchor flow totals need a year of history. Account-balance reconstruction for tax purposes needs lifetime history. Multi-year stablecoin anchor research needs every payment and path payment in the window.

Bitquery's Stellar data layer carries the full chain history from the genesis ledger to the most recent finalized ledger. A customer running a historical pull workload can request data ranges going back to early protocol versions without rebuilding from a Horizon archive node from scratch. Backfill jobs typically request multi-year windows sliced into monthly partitions, and refresh rolling thirty-day windows on top for current-period accuracy.

The granularity is at the individual operation level. Every Stellar payment operation, every path-payment operation, and every issued-asset transfer is captured as one row keyed on (hash, operation_index). No aggregation, no summarization, no down-sampling at the source. The customer's warehouse decides what to aggregate. The upstream delivers the raw transfer fact.

Stellar Specifics: SCP Finality and Multi-Asset Reality

Two characteristics of Stellar shape the way the schema gets used.

Finality through SCP, not Proof of Work. Stellar runs the Stellar Consensus Protocol. A ledger closes roughly every five seconds, and a transfer included in a closed ledger is final at the next ledger close. There is no reorg risk on Stellar the way there is on Bitcoin or any Nakamoto-consensus chain. A reconciler on Stellar does not have to wait six confirmations before crediting a deposit. The conventional rule is to credit at the next ledger close after the operation appears, which works out to a few seconds of wall-clock latency between the on-chain event and the credit hitting the customer's available balance.

Multi-asset, not single-asset. Stellar carries native XLM and an open set of issued assets that any account can issue. The biggest issued-asset flows on Stellar are Circle USDC (issuer address GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN), EURC, and a long tail of anchor-issued fiat tokens, money-market tokens, and project tokens. Every issued asset carries an (symbol, issuer) pair as its identifier, because two different issuers can issue assets with the same code. The schema captures the issuer address on every transfer row, which is what makes asset-level filtering and per-issuer reconciliation possible without a side metadata service.

Path payments are the other Stellar wrinkle worth calling out. A path payment lets a sender pay in asset A while the receiver gets asset B, with the chain finding a path through one or more order books. On the transfer schema this shows up as currency_from_* and currency_to_* differing on the same row. A reconciler that collapses them into one "currency" field will mis-book path payments. Keeping the two columns separate is the right call.

What Stellar Custodians Pull at Scale

The customers running production Stellar deposit-detection pipelines on Bitquery cluster around a few distinct use-case shapes, with consistent data needs across them. Anonymized usage ranges from the Stellar customer base:

  • Per-address walker workloads: production custodians and anchors maintaining a Stellar deposit index pull transfers for a corpus of user-facing addresses on a steady cadence, with the block cursor advancing chunk by chunk.
  • USDC and anchor-token focused workloads: a meaningful share of Stellar usage is filtered specifically to USDC, EURC, and anchor-issued fiat tokens, reflecting Stellar's role as a fiat-on-ramp and remittance settlement network.
  • Tip-of-chain heartbeats: every walker pipeline pairs with a lightweight heartbeat query against the latest ledger, polled tens of thousands of times a month at the higher end of the volume curve.
  • Per-transaction forensic lookups: ad-hoc per-hash queries against the transactions endpoint, used for "support ticket came in claiming a deposit, pull the exact transaction" investigation patterns.
  • Network metrics dashboards: combined transaction count, fee, and ledger count queries to render network-level ops dashboards that pair with the deposit pipeline.

The pattern across all of them is the same. Curate the address corpus, ingest at the per-operation granularity, write to the warehouse, run downstream reconciliation on a schema the customer controls. Bitquery handles the Stellar-specific decoding, the asset metadata resolution, and the delivery. The customer handles the attribution.

The Three-Component Architecture, on the API

For teams using the Bitquery GraphQL API directly (prototyping, smaller workloads, or interactive exploration before moving to cloud data dumps), the deposit detection loop reduces to three components.

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

The second component is a per-address walker. It calls the Stellar transfers query with a block: {gt: <last seen height>} filter and an any: [{sender}, {receiver}] clause to catch deposits and withdrawals in one call per address.

The third component is a reconciler. It joins the walker's output against the custodian's internal address book, attributes each operation to a customer account, and writes a deposit or withdrawal event to the ledger, keyed on (hash, operation_index) for idempotency.

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 ledger height and timestamp:

{
  stellar(network: stellar) {
    blocks(options: {limit: 1, desc: "timestamp.time"}) {
      height
      timestamp { time(format: "%Y-%m-%dT%H:%M:%SZ") }
    }
  }
}

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. Stellar produces a ledger roughly every five seconds, 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.

The Walker Query

The walker fetches every transfer where a watched address is either the sender or the receiver, bounded by a block cursor. The example below uses the address GBCGMODTZGGT7BKDTAKQF3T23A6NTJVKDVQGE7T6RGVJW3SP5SB7ERYZ and ledger cursor 60710224. Open the Stellar walker query in the Bitquery GraphQL IDE to run it:

{
  stellar(network: stellar) {
    transfers(
      any: [
        { sender:   { is: "GBCGMODTZGGT7BKDTAKQF3T23A6NTJVKDVQGE7T6RGVJW3SP5SB7ERYZ" }}
        { receiver: { is: "GBCGMODTZGGT7BKDTAKQF3T23A6NTJVKDVQGE7T6RGVJW3SP5SB7ERYZ" }}
      ]
      block: { gt: 60710224 }
      date: { since: "2026-05-01", till: "2026-05-29" }
      options: { limit: 100, offset: 0, asc: "block" }
    ) {
      transaction { hash }
      operation  { index name }
      currencyFrom { name address symbol tokenType }
      currencyTo   { name address symbol tokenType }
      sender   { address }
      receiver { address }
      timestamp { iso8601 }
      direction
      amountFrom
      amountTo
      block
    }
  }
}

A few things to call out about this shape.

One query, both directions. The any: [{sender}, {receiver}] clause catches deposits and withdrawals in one call. The split-direction pattern (two separate queries per address, one filtered on sender, one on receiver, merged in the application) is a common alternative and it works, but it doubles your call count and pushes the merge into your code. The any form is cleaner.

Block cursor, not time cursor. block: {gt: 60710224} is the cursor that matters. A time-based cursor drifts because two transfers can share a timestamp and reorder under you. A block cursor is monotonic, the chain assigns it, and re-running the same height produces the same result set. That makes retries safe. If your walker process dies mid-sweep, restart from the last block height you persisted and you cannot double-count.

Ascending order matters. options: {asc: "block"} lets your reconciler assume "every event with block > lastApplied is unseen." Without ascending order you have to sort in your application, and you have to wait until you have drained the whole page before advancing the cursor. With ascending order you can stream-apply.

Hash-set filter for replay. Adding a transactionHash: {in: ["..."]} clause when needed is the escape hatch. If your reconciler ever lands in a state where you suspect a specific set of transfers got mis-applied, you can pass the exact hashes back through the same query shape and replay them deterministically. One query for sweeping; same query for forensics.

Reconciliation

Each row the walker returns is one operation. The dedupe key is (transaction.hash, operation.index). A single Stellar transaction can carry up to 100 operations, and several of them can be payment or path-payment operations to the same address. Keying only on the transaction hash drops events. Keying on the pair gives you exact-once application against your ledger.

The walker is already sorted by ascending block. For each event the reconciler checks whether (hash, op_index) has been seen, applies a credit or debit if not, and persists the new cursor. SCP finality means there is no confirmation buffer to wait through. A transfer in a closed ledger is final at the next ledger close.

Filtering for One Asset

If the workload only cares about a specific issued asset (typical for a USDC-only anchor or a treasury that holds one stablecoin), narrow with currencyToName using the canonical SYMBOL [ISSUER] form Bitquery uses:

{
  stellar(network: stellar) {
    payments(
      receiver: {is: "GBCGMODTZGGT7BKDTAKQF3T23A6NTJVKDVQGE7T6RGVJW3SP5SB7ERYZ"}
      currencyToName: {is: "USDC [GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN]"}
      success: {is: true}
      block: {gt: 60710224}
      options: {limit: 100, asc: "block"}
    ) {
      block
      transaction { hash }
      operation { index }
      sender { address }
      receiver { address }
      amountTo
      currencyTo { name symbol address }
      timestamp { iso8601 }
    }
  }
}

The issuer string GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN is Circle's USDC issuer on Stellar mainnet. The same SYMBOL [ISSUER] shape works for any anchor-issued token; substitute the issuer. Pointed at a treasury or hot-wallet address, this filter pulls only USDC-denominated inflows and skips everything else.

Bitquery vs Self-Hosted stellar-core Plus Horizon

The natural alternative is running stellar-core plus Horizon plus Postgres. The node ships ledger close metadata. Horizon ingests, decodes, and exposes a query layer. Postgres carries the indexed history. The custodian's pipeline queries Horizon for transfers per address.

Dimension Self-hosted stellar-core + Horizon + Postgres Bitquery cloud data dumps Bitquery GraphQL API
Time to first data in warehouse Hours to days for initial Horizon ingestion Hours (agreement signed, bucket connected) Minutes (sign up, key, query)
Historical depth Bounded by your Horizon ingestion state Full Stellar history from genesis Full Stellar history from genesis
Disk requirements Around 500 GB and growing for full Horizon None (data lives in your own bucket / warehouse) None
Asset coverage and metadata Resolve issuer metadata yourself Pre-resolved in the currency_from_* and currency_to_* columns Pre-resolved in the currencyFrom and currencyTo objects
Operation-level granularity Available in raw form; decode yourself Captured per row, keyed on (hash, operation_index) Captured per row, keyed on (transaction.hash, operation.index)
Protocol upgrade cadence Horizon schema migrations and reingestion on protocol bumps Handled upstream Handled upstream
Operational surface Three processes (core, Horizon, Postgres) A managed bucket your warehouse reads from One HTTP endpoint
Right fit for Teams needing raw Stellar transaction envelopes or sub-millisecond local queries Production custodians, anchors, and payments products at scale Prototyping, smaller workloads, interactive exploration

The case for self-hosting reduces to a small set of operational reasons: keeping the chain-syncing process inside the team's perimeter, owning the node-level fork choice, or needing access to raw transaction envelopes or operation results beyond the published schema. For everything else (deposit detection, withdrawal reconciliation, anchor compliance reporting, multi-asset flow analytics) the Bitquery delivery options cover the data needs without the node underneath.

The cost case is worth running explicitly. A modest stellar-core plus Horizon plus Postgres fleet runs roughly three to six thousand USD per month between hardware, on-call rotation, and storage growth. A Bitquery cloud-data agreement covering the same Stellar workload is typically a fraction of the total, because the upstream decoding, history, and chain operations are amortised across every Bitquery customer rather than carried by one team's data infrastructure budget.

Multi-Chain Portability

The deposit-detection pattern above ports to other chains without architectural changes. Swap the network root, swap the cursor field name, and the same heartbeat plus walker plus reconciler runs against a different chain. The reconciler ledger logic (dedupe key, credit and debit booking) is identical across chains. Only the cursor field name and a couple of asset-handling branches change.

For a multi-chain custodian, that means the Stellar deposit pipeline shares 95% of its code with the Bitcoin pipeline, the Cardano pipeline, and the Avalanche pipeline. Sibling deposit-detection guides on Bitquery cover each of those chains explicitly: Bitcoin, Cardano, and Avalanche balance polling. All four sit on the same underlying Bitquery data layer, delivered either as cloud data dumps for enterprise workloads or queried over the GraphQL API for smaller pipelines.

If you are building this for the first time, build it for one chain first. Make sure your reconciler is idempotent against retries. Make sure your cursor is durable. Then add the next chain. That should be a day of work, not a quarter.

Summary

A custodial Stellar product detecting deposits and withdrawals at scale has a clean enterprise path through Bitquery. Sign up for cloud data dumps. Connect a warehouse to the S3 bucket, Snowflake account, or Google Cloud destination. Ingest at the team's own cadence. Run downstream reconciliation against the team's own schema.

The Stellar data layer carries full chain history at the per-operation granularity, with the schema (identification, ledger context, counterparty, value, currency-from and currency-to) pre-decoded into typed fields the warehouse can use without a separate decoder. Issued-asset metadata is pre-resolved. Path payments are first-class on the schema, with currency_from_* and currency_to_* populated independently. SCP finality means deposits clear at the next ledger close, with no reorg buffer to wait through.

For smaller workloads, prototyping, and interactive exploration, the same data is available over the Bitquery GraphQL API. The walker, heartbeat, and reconciler patterns covered above run unchanged from there.

The sample data files and the S3 bucket link for end-to-end testing are in the public sample repository under the Stellar directory.

Related Resources

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.