Your liquidation bot fires on a price that is 90 seconds old.
The market already moved 3%. A healthy position gets closed, and the user files a ticket.
Oracle bugs rarely look like bugs. They look like a stale price or a wide quote.
This guide shows you how to read oracle prices in a Solana program and reject the bad ones.
Save the 7-point oracle checklist near the end for your next audit.
How the Oracle Gets a Price Into Your Program
Pyth is a pull oracle. Prices don't sit on Solana waiting for you. Your client fetches a signed price update and posts it in the same flow as your own instruction.
Your program then reads that update from an account and checks it. If the update is too old or for the wrong feed, the read fails. Your instruction stops there.
Two account types
The official Solana integration guide describes two ways to hand a price to your program:
- Price update accounts are short-lived. Anyone can create, overwrite, and close them. Use them when you need a price for a specific moment, like a trade or a liquidation.
- Price feed accounts have a fixed address per feed and always move forward in time. Use them when you only ever want the latest price.
The network sponsors updates for a set of common feeds on shard 0 of the price feed accounts. For any other feed, you or a price pusher service must keep the account fresh. Your program code reads both types the same way.
What changed in 2026
The same guide says all Core contracts were upgraded on August 26, 2026, and kept their interface. Updates now come from Hermes, which collects prices signed by the Pro routers.
Two practical changes follow. Hermes now needs an API key, and the guide points at a new Hermes endpoint. If your client still calls an old public URL with no key, test it before you trust it in production.
Set Up the Rust and TypeScript SDKs
You need one crate on-chain and two packages off-chain. The integration guide lists them.
# programs/your_program/Cargo.toml
[dependencies]
anchor-lang = "0.32.1"
pyth-solana-receiver-sdk = { version = "1.2.0", features = ["pro-compatible"] }npm install --save @pythnetwork/hermes-client @pythnetwork/pyth-solana-receiverThe pro-compatible feature points the crate at the newer compatible programs. Without the flag, the crate uses the original programs. Both still work.
Pin versions on purpose. The guide says the feature needs crate version 1.2.0 or later.
It also pairs versions with Anchor. Version 1.2.0 works with Anchor 0.32.1, and version 2.0.0 works with Anchor 1.0. The example above pins the Anchor 0.32.1 pair. If you're on Anchor 1.x, use pyth-solana-receiver-sdk 2.0.0 instead, and check the crate page for the latest release before you lock the file.
Read a Price Safely in an Anchor Program
Add a PriceUpdateV2 account to your instruction context. With Anchor's Account type, the framework checks that the oracle's pull program owns it. If you skip Anchor, write that owner check yourself.
Then read the price with a staleness limit and a feed ID. This example also rejects quotes whose confidence band is too wide.
use anchor_lang::prelude::*;
use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, PriceUpdateV2};
// BTC/USD feed ID from the official docs
const BTC_USD: &str = "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";
const MAX_AGE_SECS: u64 = 30;
const MAX_CONF_BPS: u128 = 100; // reject if conf > 1% of price
#[derive(Accounts)]
pub struct Liquidate<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub price_update: Account<'info, PriceUpdateV2>,
}
pub fn liquidate(ctx: Context<Liquidate>) -> Result<()> {
let feed_id = get_feed_id_from_hex(BTC_USD)?;
// Fails if the update is older than MAX_AGE_SECS or for another feed.
let p = ctx.accounts.price_update
.get_price_no_older_than(&Clock::get()?, MAX_AGE_SECS, &feed_id)?;
require!(p.price > 0, OracleError::BadPrice);
let conf_bps = (p.conf as u128) * 10_000 / (p.price as u128);
require!(conf_bps <= MAX_CONF_BPS, OracleError::WideConfidence);
// p.price * 10^p.exponent is the USD price. Do your math in fixed point.
Ok(())
}
#[error_code]
pub enum OracleError {
#[msg("Oracle price is not positive")]
BadPrice,
#[msg("Oracle confidence band too wide")]
WideConfidence,
}The 30-second limit matches the official example. Treat it as a ceiling, not a target. Lending and derivatives apps often want a tighter limit, which the checklist below covers.
The 1% confidence cap is our example, not a published number. Tune it per market. A stablecoin feed and a small-cap token feed need different caps.
Post Price Updates From Your Client
On the client, you fetch the signed update from Hermes and post it. The receiver's transaction builder then adds your own instruction, which reads the posted account.
import { HermesClient } from '@pythnetwork/hermes-client';
import {
PythSolanaReceiver,
PRO_COMPATIBLE_PUSH_ORACLE_PROGRAM_ID,
PRO_COMPATIBLE_RECEIVER_PROGRAM_ID,
PRO_COMPATIBLE_WORMHOLE_PROGRAM_ID,
} from '@pythnetwork/pyth-solana-receiver';
const BTC_USD = '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43';
const hermes = new HermesClient('https://pyth.dourolabs.app/hermes', {
accessToken: process.env.PYTH_API_KEY,
});
const updates = (await hermes.getLatestPriceUpdates([BTC_USD], { encoding: 'base64' })).binary.data;
const receiver = new PythSolanaReceiver({
connection,
wallet,
pushOracleProgramId: PRO_COMPATIBLE_PUSH_ORACLE_PROGRAM_ID,
receiverProgramId: PRO_COMPATIBLE_RECEIVER_PROGRAM_ID,
wormholeProgramId: PRO_COMPATIBLE_WORMHOLE_PROGRAM_ID,
});
// true = close the update account at the end and reclaim rent
const builder = receiver.newTransactionBuilder({ closeUpdateAccounts: true });
await builder.addPostPriceUpdates(updates);
await builder.addPriceConsumerInstructions(async (getPriceUpdateAccount) => {
const ix = await program.methods
.liquidate()
.accounts({ priceUpdate: getPriceUpdateAccount(BTC_USD) })
.instruction();
return [{ instruction: ix, signers: [] }];
});
const txs = await builder.buildVersionedTransactions({ computeUnitPriceMicroLamports: 50000 });
await receiver.provider.sendAll(txs, { skipPreflight: true });Keep the API key on a server. If your frontend needs prices, route the fetch through your backend. The key never ships to a browser.
Posting and fully verifying an update takes more than one transaction today. If you need a single transaction, the guide offers a partly verified path. Read the data integrity trade-off in the TypeScript receiver SDK before you switch.
The 7-Point Oracle Checklist
The official best practices page is short and worth a full read. Here is how to turn it into checks before an audit.
- Staleness limit per action. Opening a position can tolerate more age than a liquidation. Set each limit on purpose.
- Feed ID pinned. Hard-code or store the feed ID and check it on every read.
- Owner check. Use Anchor's account type, or verify the owner by hand.
- Confidence gate. Pause price-based actions when the band is wide relative to the price.
- Side-aware bounds. Decide which edge of the band each side of a trade uses, so a bad print can't hurt you.
- Market hours. The network doesn't promise a current price for every product at all times. Equity feeds go quiet when their markets close.
- Update ordering. The sender picks which signed update to post. Reject updates older than the last one you used.
Point 7 is the one teams skip most often. The best practices page notes that picking an update works like a latency edge. A tight staleness check is the simplest defense.
When you don't need an oracle
Not every price needs one. On Pump Science, a historical case study, each longevity compound got its own token on a bonding curve. The build added dynamic fees and automated liquidity migration to a DEX. The curve set the price, so no outside feed sat in the hot path.
Use an oracle when your program must agree with a market outside itself. If you need help drawing that line, our Solana DeFi development team builds and reviews this logic.
FAQ
How old can an oracle price be before I reject it?
Pick a limit per action, and keep it as tight as your users can tolerate. The official Solana example uses 30 seconds as a starting point. The best practices page says latency-sensitive apps may cut that to a few seconds.
Do I need an API key to read prices on Solana?
Yes, to fetch updates from Hermes. The current guide says Hermes requires an API key, passed as an access token in the client. Reading a sponsored price feed account on-chain doesn't need a key, since someone else posts those updates.
What is the confidence interval?
It's the oracle's estimate of how far the true price might sit from the published one. Each price ships with a confidence value around it. A wide band means the market disagrees, and that's when your program should slow down or pause.
Should I use price feed accounts or price update accounts?
Use price feed accounts when you always want the latest price at a fixed address. Use price update accounts when you need the price at a specific moment, like a trade. Your on-chain code reads both the same way.
Did the August 2026 upgrade break existing integrations?
The official guide says the Core contracts kept their interface in the August 26, 2026, upgrade. The bigger client change is that Hermes now requires an API key. Test your fetch path on a staging setup before you assume it still works.
Ship Price Logic You Can Defend
Oracle code is small and easy to get wrong. Most of the risk sits in the checks around the read, not the read itself.
Designing a new oracle path? Our Architecture Sprint runs $3K to $5K and covers a pre-launch design review like this one. If your oracle path is already live and failing, Reliability and Rescue runs $5K to $15K. Try the checklist above first, then tell us what your program prices.

