Pyth: the on-chain price layer, Push vs Pull, and the price account schema
Pyth aggregates publisher-submitted prices and publishes them on-chain. Here's the price account shape, Push vs Pull models, and how to read a feed in code.
Pyth is the price oracle that most major Solana DeFi protocols (Drift, Kamino, marginfi, MarginFi, etc.) consume. It aggregates prices submitted by ~100 first-party publishers — exchanges, market makers, trading firms — into a single price account per symbol, published on-chain every Solana slot.
Two distribution models matter: Push (Solana- native, account-based) and Pull (cross-chain, message-based). Same data, different integration shape.
The publisher → aggregate flow
Every Pyth symbol has a fixed set of authorised publishers. Each slot, each publisher submits its current price + a confidence interval to the Pyth oracle program. The program aggregates these into a single canonical price with a derived confidence, weighted by publisher reliability and recency.
Slot N: Publisher 1 submits price=100.05, conf=0.10
Publisher 2 submits price=100.02, conf=0.08
Publisher 3 submits price=99.98, conf=0.12
...
Aggregator computes price=100.01, conf=0.09
Aggregate written to the on-chain PriceAccount for this symbolOnce aggregated, the price lives in a deterministic PriceAccount at a fixed address per symbol. Any consumer can getAccountInfo on that address and read the current price.
The PriceAccount schema (Push model)
pub struct PriceAccount {
pub magic: u32, // 0xa1b2c3d4 — version marker
pub version: u32,
pub atype: u32, // 3 = Price
pub size: u32, // bytes used
pub price_type: u32, // 1 = Price (vs TwapAccount etc)
pub exponent: i32, // applied to price → actual_price = price * 10^exponent
pub num: u32, // num publishers
pub num_qt: u32, // num successful quotes this slot
pub last_slot: u64,
pub valid_slot: u64, // last slot this price was valid in
pub ema_price: PriceInfo, // exponentially-weighted moving average
pub ema_conf: PriceInfo,
pub timestamp: i64,
pub min_pub: u8, // minimum publishers required for "trading" status
pub message_sent: u8,
pub max_latency: u8,
pub drv3: u8,
pub drv4: u32,
pub product: Pubkey, // → ProductAccount with metadata (symbol, asset_type)
pub next: Pubkey, // legacy
pub prev_slot: u64,
pub prev_price: i64,
pub prev_conf: u64,
pub prev_timestamp: i64,
pub agg: PriceInfo {
price: i64, // the aggregated price (scaled by exponent)
conf: u64, // confidence interval (1-sigma uncertainty)
status: u32, // 1=Trading, 2=Halted, 3=Auction, 4=Ignored, 0=Unknown
corp_act: u32,
pub_slot: u64,
},
pub components: [PriceComponent; 32], // per-publisher data
}The exponent
Pyth stores prices as i64 with a separate exponent: i32. To get the actual decimal price:
actual_price = agg.price × 10^exponent
Example for SOL/USD:
agg.price = 23045123456
exponent = -8
→ actual = 23045123456 × 10^-8 = $230.45123456Always apply the exponent. Hardcoding decimals breaks every time Pyth adjusts a feed's precision (rare but happens).
Reading a Pyth price on Solana
use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, PriceUpdateV2};
#[derive(Accounts)]
pub struct UsePyth<'info> {
pub price_update: Account<'info, PriceUpdateV2>,
}
pub fn read_price(ctx: Context<UsePyth>) -> Result<()> {
let price_update = &ctx.accounts.price_update;
let feed_id = get_feed_id_from_hex(
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", // SOL/USD
)?;
// get_price_no_older_than enforces a maximum age (seconds)
let price = price_update.get_price_no_older_than(
&Clock::get()?,
60, // max age in seconds
&feed_id,
)?;
// price.price is i64 scaled by 10^price.exponent
msg!("SOL/USD: {} (conf {}, exp {})", price.price, price.conf, price.exponent);
Ok(())
}Push vs Pull
Push (Solana-native). Publishers submit to a single Pyth oracle program; the aggregated price account is updated every slot, automatically. Consumers just getAccountInfo. Used by every Solana-native protocol because there's no extra step.
Pull (cross-chain). Pyth's aggregator lives on Pythnet (a separate appchain). Prices are signed by a Wormhole-style guardian set and relayed to consumer chains on-demand. Consumers fetch a signed price update from Hermes (Pyth's API), include it as an instruction in their transaction, and the on-chain Pyth Receiver verifies + writes it. Used on EVM chains and now also offered on Solana for fresher prices when needed.
// Pull pattern on Solana — fetch a signed update from Hermes, include in tx
import { HermesClient } from "@pythnetwork/hermes-client"
const hermes = new HermesClient("https://hermes.pyth.network")
const updates = await hermes.getLatestPriceUpdates([
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", // SOL/USD feed_id
])
// updates.binary.data contains the wire-format updates to pass to
// the Pyth Receiver program's post_update instruction.Using the confidence interval
Pyth's killer feature is the confidence interval (1-sigma uncertainty). Most oracles give you a price; Pyth gives you a price and how sure it is. Production DeFi protocols reject trades when confidence widens past a threshold:
// Reject prices where uncertainty > 1% of price
let conf_pct = (price.conf as f64) / (price.price as f64).abs();
require!(conf_pct < 0.01, MyError::PriceTooUncertain);
// Reject stale prices (>60 seconds since update)
let age = clock.unix_timestamp - price.publish_time;
require!(age < 60, MyError::PriceStale);Without these checks, your protocol takes whatever Pyth happens to publish during low-quality data periods (publisher outages, thin liquidity moments). The checks above are the minimum viable defensive posture.
References
- docs.pyth.network
- Pyth price feed IDs (canonical addresses per symbol)
- pyth-network/pyth-crosschain — Pull model implementation
Pyth is the price layer most Solana DeFi reads from. Apply the exponent, enforce age limits, gate on confidence — those three habits cover 90% of oracle-related production incidents.