How it works
A Pyth price travels through five clear steps before your contract reads it. Each step adds a property the next one needs: signatures, aggregation, availability, freshness, on-chain trust. Once you see the chain of trust, the rest of the docs make sense.
The five steps
- Publish. A trading firm — say, Jane Street — runs a publisher client. Every 400ms it signs and submits a quote: BTC/USD = 67,432.15 ± 3.20. The signature uses the publisher's own key. No one else can fake it.
- Aggregate. All publisher quotes for the same feed land on Pythnet, a small Solana fork dedicated to oracle work. Pythnet runs a weighted median that ignores outliers and produces one aggregate price plus an aggregate confidence interval.
- Relay. Pythnet emits the aggregate as a price update message, signed by the Pythnet validator set, and broadcast through Wormhole. The signed payload is sometimes called a VAA — Verifiable Action Approval.
- Serve. A web service called Hermes caches the latest signed update for every feed. Anyone can pull it over HTTP or stream it over WebSocket. No API key, no rate limit auth.
- Post. Your contract takes the signed update, calls
updatePriceFeeds()on the Pyth contract on its own chain, and then reads the fresh value withgetPriceNoOlderThan().
Why this is called a pull oracle
A real handoff, in code
The next snippet is the bare minimum to use Pyth on any EVM chain. Three steps: read the latest signed update from Hermes off-chain, send it as bytes[] with your transaction, call the Pyth contract.
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
contract MyContract {
IPyth pyth;
bytes32 ethUsdId = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace;
constructor(address pythAddr) { pyth = IPyth(pythAddr); }
function settle(bytes[] calldata priceUpdate) external payable {
uint fee = pyth.getUpdateFee(priceUpdate);
pyth.updatePriceFeeds{value: fee}(priceUpdate);
PythStructs.Price memory p = pyth.getPriceNoOlderThan(ethUsdId, 60);
// p.price, p.conf, p.expo, p.publishTime
}
}On the frontend you fetch the update from Hermes:
import { HermesClient } from "@pythnetwork/hermes-client";
const hermes = new HermesClient("https://hermes.pyth.network");
const ethUsdId = "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
const updates = await hermes.getLatestPriceUpdates([ethUsdId]);
const updateData = updates.binary.data.map(d => "0x" + d);
await contract.settle(updateData, { value: ethers.parseEther("0.0001") });Who signs what
Trust is layered. If you only had one signature you would have to trust whoever made it. Pyth has three:
- Publisher signatures. Each price submission is signed by the publisher's own key. Anyone can verify who submitted what.
- Pythnet aggregate. A supermajority of Pythnet validators must agree on the median before it becomes the official price.
- Wormhole guardian signatures. A separate set of 19 guardians signs the cross-chain message that carries the price off Pythnet. Your contract verifies their signatures, not the publishers' directly.
What you do not have to do
Most teams over-engineer their oracle integration on the first pass. You do not need:
- A keeper bot that polls and posts every block. Post the update inside the transaction that needs it.
- Your own off-chain price cache. Hermes is free and fast.
- A complex retry queue. If a transaction reverts, the next one fetches a fresher update anyway.
The one rule everyone breaks
getPriceNoOlderThan(feedId, maxAge), not getPrice(). The plain getter will happily return a stale price from last week. The bounded version reverts, which is what you want.