Pyth price feeds
Solana programs are sandboxed. They cannot fetch a stock price, call a REST API, or read a database. Anything that comes from outside the chain has to be put on chain by something, and that something is called an oracle. This lecture covers what oracles actually are, why they matter for any serious DeFi protocol, how Pyth's first-party publisher architecture solves the trust problem, and how to consume a feed safely from a Solana program.
The oracle problem
The Solana runtime is a deterministic state machine. Every validator must execute every transaction and arrive at the same result, otherwise consensus breaks. That requirement rules out anything that could return different values to different validators: HTTP requests, random number generators, file system reads, system clocks beyond the Clock sysvar. None of these are available inside a Solana program, and no compiler flag will give them to you.
This is a problem because most interesting financial applications depend on real-world data. A lending protocol needs to know what its collateral is worth, in dollars, right now, so it can liquidate undercollateralized loans. A stablecoin needs to know what the dollar is doing to maintain its peg. A derivatives platform needs to know the price of whatever asset its contracts settle against. An insurance program needs to know whether a flight was delayed, whether a hurricane hit, whether a parametric trigger condition was met.
None of this data exists on chain natively. The only way to get it there is for someone outside to write a transaction that puts it there. That someone is an oracle.
What "an oracle" actually is
The word makes oracles sound mysterious. They are not. An oracle, mechanically, is two things:
- A program on chain that owns accounts holding some data.
- An off-chain operator who sends transactions that update that data.
Your program reads the oracle's account the same way you read any other account, by deserializing it through an SDK or directly. The interesting question is not how the read works, since it's just an account read. The interesting question is who you trust to do the writing.
That trust question is the entire field. If the operator decides to publish a fake price, your program has no way to tell. If the operator's infrastructure goes down, your program reads a stale value. If the operator is compromised, your protocol is compromised. The whole engineering effort around production oracle systems is about reducing how much you have to trust any single party.
Why single-source oracles are dangerous
The simplest possible oracle is one program, one operator. The operator queries one data source, signs a transaction with the result, and writes it on chain. This works fine for a hobby project. It is unacceptable for anything holding real funds.
Three failure modes break a single-source oracle:
- The source is wrong. Even reputable exchanges have brief glitches. An API returns the price of a different asset for a few seconds. Volume thins out and the last trade is far from fair value. If your oracle pulls from one place, you inherit every glitch from that place.
- The operator is malicious. A single party with the ability to push any number on chain has every incentive to do so when the payoff is large enough. The history of DeFi exploits includes cases where compromised oracle keys were used to drain protocols of tens of millions of dollars in single transactions.
- The operator goes offline. The oracle stops updating. Your program continues to operate using an increasingly stale price. By the time anyone notices, positions that should have been liquidated have moved against the protocol.
Any one of these is enough to lose the entire treasury. Real protocols cannot use a single-source oracle and expect to survive a market move.
How Pyth solves it
Pyth attacks the problem with a different architecture from most oracles. Where Chainlink and similar systems use third-party nodes that pull data from public APIs, Pyth has the data sources themselves as publishers. Jane Street, Wintermute, Binance, OKX, Cboe Global Markets, and dozens of others run publisher software that posts their internal prices directly to Pyth. These are first-party publishers writing their own books to the oracle, rather than third-party nodes scraping a public API.
The aggregation happens on Pythnet, a Solana-fork appchain dedicated to oracle data. Each publisher writes their price observation as a transaction on Pythnet. Pythnet validators aggregate the observations into a single price plus a confidence interval, and the aggregate is propagated to Solana mainnet every slot. By the time your program reads a Pyth price account, the data has been through deduplication, outlier rejection, and confidence-weighted averaging across many publishers.
The Pythnet layer is the cost-saving move equivalent to Chainlink's OCR. Without it, every publisher would have to write their own price to Solana mainnet, paying fees, and the aggregation would happen on-chain at enormous compute cost. Pythnet lets the publishers do their work on a separate chain and lets only the final aggregate touch Solana.
The on-chain side on Solana is a set of accounts owned by the Pyth program. Each price feed has its own account at a stable address. Your program reads from that account just like any other Anchor account read.
Reading a feed
Consuming a Pyth price feed is one account read and a deserialize call. Add pyth-sdk-solana to your Cargo.toml, and the consumer looks like this:
use anchor_lang::prelude::*;
use pyth_sdk_solana::load_price_feed_from_account_info;
declare_id!("...");
#[program]
pub mod price_consumer {
use super::*;
pub fn read_price(ctx: Context<ReadPrice>) -> Result<()> {
let price_account_info = &ctx.accounts.price_feed;
let price_feed = load_price_feed_from_account_info(price_account_info)
.map_err(|_| OracleError::InvalidPriceFeed)?;
// Get the current price, rejecting it if older than 60 seconds
let current = price_feed
.get_price_no_older_than(&Clock::get()?, 60)
.ok_or(OracleError::StalePrice)?;
msg!(
"price: {} (conf {}) x 10^{}",
current.price,
current.conf,
current.expo
);
Ok(())
}
}
#[derive(Accounts)]
pub struct ReadPrice<'info> {
/// CHECK: validated by the Pyth SDK loader
pub price_feed: AccountInfo<'info>,
}
#[error_code]
pub enum OracleError {
InvalidPriceFeed,
StalePrice,
}The load_price_feed_from_account_info call deserializes the account, verifies it is owned by the Pyth program, and returns a PriceFeed struct. get_price_no_older_than returns the current price if publish_time is within the threshold, or None if not. Combining both checks into one call is the recommended pattern, since it handles validity and staleness together.
The result has four fields you care about:
price: the price as a signed integer (i64)conf: the confidence interval, alsoi64, expressed in the same scale as the priceexpo: the exponent, almost always negative, so the actual price isprice * 10^expopublish_time: when the price was last updated, as a unix timestamp
The price comes back as i64. Pyth uses signed integers because some feeds report values that can legitimately be negative, like interest rate differentials. For ETH/USD, the value will always be positive, but the type accommodates the full range.
Decimals via the exponent
Pyth feeds use an exponent rather than a fixed decimals count. Each feed has its own expo value, accessible on the price struct.
For ETH/USD, the exponent is typically -8. If the feed returns price = 350000000000, the actual price is 350000000000 * 10^-8 = 3500.00000000 USD. To use this in computation alongside an SPL Token amount, whose decimals live on the mint, you need to align the exponents:
let raw_price = current.price;
require!(raw_price > 0, OracleError::NegativePrice);
let price_u64 = raw_price as u64;
// Scale to 18 decimal places for downstream math
// If expo = -8, we multiply by 10^(18 - 8) = 10^10
let adjustment = (18 + current.expo) as u32;
let price_18_dec = price_u64
.checked_mul(10u64.pow(adjustment))
.ok_or(OracleError::Overflow)?;The require!(raw_price > 0) check guards against the edge case of a misconfigured feed returning zero or negative. For a USD-quoted equity or crypto feed, the price should never reach zero. Doing this once at the read site is much cheaper than verifying everywhere downstream.
If you call the feed's metadata once at initialization and cache the exponent on your config account, you save the read on every consumption. The exponent does not change for a given feed.
Confidence intervals
Pyth reports more than just a price. It reports a price AND a confidence interval, expressed in the same units as the price. A return value of price = 350000000000, conf = 10000000 at expo = -8 means "ETH/USD is approximately 3500.00, with a one-sigma confidence of about 0.10."
This is unique among major oracles and matters more than it sounds. The confidence interval reflects how much publishers disagree. In a calm market with deep liquidity across exchanges, publishers converge on nearly the same price and confidence is tight. During a market dislocation, like a flash crash, a major exchange going offline, or a publisher's price feed lagging, publishers diverge and confidence widens.
A wide confidence interval is a signal that the price you're reading may not reflect a single coherent market reality. A risk-conscious protocol can refuse to act on data with confidence wider than some threshold:
let price = current.price as u64;
let conf = current.conf;
// Reject if confidence is wider than 1% of the price
// conf * 100 <= price is equivalent to conf / price <= 0.01
require!(
conf.checked_mul(100).ok_or(OracleError::Overflow)? <= price,
OracleError::ConfidenceTooWide
);Setting the threshold is application-specific. A lending protocol with conservative liquidation parameters might require confidence within 0.5%. A perpetual exchange willing to take a wider price band might accept 2%. What matters is that the check exists. A price without a confidence check assumes the oracle is always trustworthy, which is exactly the assumption Pyth's architecture was designed to avoid.
Staleness checks
A Pyth feed publishes continuously. Every Solana slot, the aggregator on Pythnet emits a new price, and the updated value lands on Solana mainnet shortly after. The on-chain price tracks the real-world price within a few hundred milliseconds under normal conditions.
That said, updates can stop. Solana network congestion can delay them. Publishers can lag during market stress. The Pyth program itself does not enforce a minimum update frequency on its readers. The price account just sits at whatever value was last written. Your program is responsible for checking the data is recent enough.
This is what get_price_no_older_than(&Clock::get()?, 60) in the consumer code does. It accepts the price if publish_time is within the last 60 seconds, and returns None if not. A threshold of 60 seconds is appropriate for most active DeFi use cases on Solana, since updates are sub-second under normal conditions. Setting it tighter, say 10 seconds, gives you fresher data at the cost of more frequent false rejections during minor network hiccups. Setting it looser, say 300 seconds, lets your protocol act on data that may no longer reflect reality.
The exact threshold depends on your protocol's tolerance. A liquidation engine running every block needs tight freshness. A daily settlement program that runs once a day can accept far older data.
A note on the pull oracle model
Everything above describes Pyth's continuous-push model on Solana. Pyth's infrastructure continuously updates the price account, and your program just reads it. This is the path most Solana programs use today, and it is the simpler mental model.
Pyth also offers a pull oracle model where the price update message itself is included in the user's transaction. The user fetches a signed price update off-chain from Pyth's Hermes service, attaches it to their transaction, and your program posts the update to the price account before reading. The pull model uses the pyth-solana-receiver-sdk crate and a slightly different consumer pattern. It is useful when you want the freshest possible data at the exact moment of a transaction, or when you want to atomically tie a price observation to a specific user action. For most consumer use cases, the push model is simpler and sufficient.