Developers

Two Ways to Read Chainlink Prices on Stellar: Data Feeds and Data Streams

Author

Stellar Development Foundation

Publishing date

Editor's note: The Stellar network's oracle ecosystem is expanding. This post is a technical guide for developers and teams building on the Stellar network. It covers how Chainlink Data Feeds and Data Streams work in Stellar smart contracts, following their mainnet launch, alongside other available providers. The Stellar Development Foundation (SDF) does not endorse, recommend, or warrant any specific oracle provider, and this post is not investment, financial, or legal advice. Developers should conduct their own due diligence before integrating any third-party service.

Chainlink Data Feeds and Data Streams are now live on Stellar mainnet, bringing Chainlink's market-leading oracle infrastructure and high-quality data to developers building on Stellar. With these two complementary data delivery models, developers can access reliable onchain reference prices for applications such as lending markets and stablecoins, or low-latency, cryptographically verifiable market data on demand for time-sensitive applications such as perpetuals and liquidations.

Which one fits depends on how fresh your prices need to be and who pays to put them onchain. This post walks through both. In the spirit of “don't trust, verify,” it also shows how to check the deployments yourself, from reading a live mainnet price to showing the Streams verifier rejects tampered data, without spending a single stroop.

Who should look at this first

Chainlink on Stellar is useful if you're building:

  • Lending markets, vaults or stablecoin logic that need reliable prices for XLM, BTC, ETH, USDC, USDT or EURC and need a shared, regularly updated reference rate
  • Perpetuals, options or liquidation engines that need a fresh price at the moment a transaction executes
  • Protocols already leveraging Chainlink on other blockchains that want to expand to Stellar using the same secure oracle infrastructure

What's live at launch

Data Feeds on Stellar mainnet cover nine assets at the time of writing, all priced against USD (feeds change over time, so treat the table below as a snapshot). The Stellar feed list has the data_id for each one.

Feed

Asset class

Threshold (If a price changes by 0.5% or more, a new value will be delivered)

Heartbeat (In the case that the threshold is unbroken for 24 hours, the data will be refreshed from the source)

XLM / USD

Crypto

0.5

24h

BTC / USD

Crypto

0.5

24h

ETH / USD

Crypto

0.5

24h

LINK / USD

Crypto

0.5

24h

USDC / USD

Stablecoin

0.5

24h

USDT / USD

Stablecoin

0.5

24h

EURC / USD

Stablecoin

0.5

24h

XAUM reference rate

Commodity (gold)

0.1

24h

FLHY / USD

RWA

0.5

24h

Choosing between Chainlink Data Feeds and Data Streams

Both solutions provide access to Chainlink market data, but serve different application requirements.

Data Feeds are designed for applications that need regularly updated onchain reference prices, such as lending markets, stablecoins, and vaults. Prices are updated based on predefined deviation thresholds and heartbeat intervals, allowing smart contracts to read the latest available data directly onchain.

Data Streams are designed for applications that need fresh market data at execution, such as perpetual exchanges and liquidation engines. Applications retrieve signed price reports offchain and submit them for onchain verification when needed.

The right choice depends on your application's requirements for data freshness, update frequency, supported assets, and transaction costs. For applications managing volatile collateral or user funds, developers should also implement appropriate freshness checks and risk controls.

How the integration works

Data Feeds: a push model

Chainlink Data Feeds provide reliable, decentralized market data for smart contracts that depend on consistently available onchain reference prices. By aggregating data through Chainlink oracle networks, Data Feeds reduce the need for developers to source, aggregate, and maintain their own pricing infrastructure.

On Stellar, a single proxy contract serves every feed. You don't need a separate address per asset, you pass a 32-byte data_id.

  • Mainnet proxy: CDQBHEUGLDHMSUHG4IH33RJBUTMNCZOT4JIAQR4WZFRDZW6DR6S32ZLZ
  • Testnet proxy: CBUF6IADAWPWIDWRTHJNINKXHG2UTIQ6TU2F2HRAXWAI7OT3KJPKK6O4

Your contract reads a feed with one cross-contract call:

#[contractclient(name = "DataFeedsProxyClient")]
pub trait DataFeedsProxy {
    fn latest_round(env: Env, data_id: BytesN<32>, decimals: u32) -> Result<Round, ProxyReadError>;
}

pub fn read_latest_price(env: Env, proxy: Address, data_id: BytesN<32>) -> I256 {
    let client = DataFeedsProxyClient::new(&env, &proxy);
    // The generated client returns the Round directly and panics on error;
    // use try_latest_round to handle ProxyReadError yourself.
    let round = client.latest_round(&data_id, &18);
    round.answer
}

Snippet adapted from Chainlink's Stellar Data Feeds guide, which has the full working example, including the Round and ProxyReadError types.

Each Round returns round_id, answer (an I256) and timestamp. Feeds are stored at 18 decimals. If you ask for fewer, the answer is truncated, not rounded, so factor that in.

💡 Tip: The sample returns the answer without looking at timestamp. In production, compare timestamp to the ledger time and reject prices older than the feed's heartbeat. That's 24 hours for the current Stellar feeds.

Data Streams: a pull model

With Streams, nothing is written onchain until your application needs a price:

1. Offchain, you fetch a signed report from Chainlink's Data Streams API through REST, WebSocket, or the Go, TypeScript or Rust SDKs. It's a subscription product.

2. Onchain, a verifier contract checks the signatures. The mainnet verifier is CAKA3NBYPC6OBEUEGNIYGNYG3ES2GPQK736B5SR7ASGUXRDAKXI2JCQI. Its verify function parses the report, looks up the registered oracle configuration, recovers each signer via ECDSA, and requires exactly f+1 valid, unique signatures from registered oracles. It checks who signed a report, not how old it is, so decode the report's observationsTimestamp and expiresAt fields and enforce your own freshness rules.

3. Your contract consumes the verified bytes:

pub fn consume_price_data(env: Env, verifier_address: Address, signed_report: Bytes, sender: Address) -> Bytes {
    sender.require_auth();
    let verifier = VerifierClient::new(&env, &verifier_address);
    // Verifies all ECDSA signatures and returns the decoded report data.
    let report_data = verifier.verify(&signed_report, &sender);
    // First 32 bytes are the feed_id; remaining fields are feed-specific.
    report_data
}

Snippet from Chainlink's Stellar Data Streams tutorial. Chainlink notes the example is unaudited.

Verify the deployment yourself

We ran every command below before publishing, against mainnet and testnet. They show the contracts behave as described; they're not a substitute for an audit. Always confirm contract addresses against Chainlink's official documentation before integrating; addresses may change.

Everything below uses Stellar RPC simulation (--send=no): no transaction is submitted and no fees are paid. Simulation runs the contract's real WASM against real ledger state, so the results reflect current contract behavior at the time you run them. Use any existing account's public address (G…) as --source-account; no secret key is needed. Any Stellar RPC provider works in place of the one shown.

1. Read a mainnet feed. Read the XLM/USD feed from the mainnet proxy:

stellar contract invoke \
  --id CDQBHEUGLDHMSUHG4IH33RJBUTMNCZOT4JIAQR4WZFRDZW6DR6S32ZLZ \
  --rpc-url https://mainnet.sorobanrpc.com \
  --network-passphrase "Public Global Stellar Network ; September 2015" \
  --source-account <ANY_PUBLIC_ADDRESS> --send=no \
  -- latest_round \
  --data_id 017be2f55a000332000000000000000000000000000000000000000000000000 \
  --decimals 18

description returns "XLM/USD", and latest_round returns the current round, for example {"answer":"176026152984863900", "round_id":1637, "timestamp":1792426484}, which is $0.1760 at 18 decimals at the time of writing. Passing an unknown data_id fails with Error(Contract, #50), which is NoDataPresent.

2. Verify a genuine Streams report. Chainlink's tutorial publishes a real signed testnet report. Pass it straight to the testnet verifier (CA7GVHWH4GRHE6GI7MHEKQZAOYO4GE7KRGSU3EOS3HYJRVLX3XEA4ONQ) with -- verify --signed_report <hex> --sender <ANY_PUBLIC_ADDRESS>. Simulation returns a verified event and the decoded report: feed ID, timestamps, and benchmark, bid and ask prices.

3. Try to cheat. Change one byte of the benchmark price inside the report and call again:

error: transaction simulation failed: HostError: Error(Contract, #12)

Error #12 is BadVerification. Changing the report means the recovered signer no longer matches a registered oracle, so the verifier rejects it. This shows the verifier rejecting a modified report.

4. Confirm mainnet is separately configured. Send the same genuine testnet report to the mainnet verifier. It fails with Error(Contract, #9), which is DigestNotSet. Mainnet only accepts reports signed under its own registered oracle configuration. is_initialized on the mainnet verifier returns true.

Disclaimer: SDF provides this post for informational and educational purposes only. References to specific projects, products, or services do not constitute an endorsement, recommendation, or warranty by SDF. Code examples are illustrative and have not been audited by SDF; developers should independently verify all code before deploying to mainnet. SDF is not responsible for the performance, security, or availability of any third-party oracle or data feed. Nothing in this post is intended as investment, financial, or legal advice.