Price Oracle Design#
Settlement depends entirely on price data: the outcome of every market is decided by comparing a start price to an end price. Seesaw uses Pyth Network as its exclusive oracle — and has made deliberate choices about how that oracle is integrated.
Why Pyth#
A prediction market needs price data that is accurate, fresh, consistently available, and hard to manipulate — with no single point of trust. Pyth fits those requirements on Solana:
- Aggregated and decentralized. Each Pyth price is aggregated from many independent publishers, so no single source can move it.
- Native to Solana. Prices are read directly from on-chain accounts with no bridge, keeping latency low and removing bridge risk.
- Broad coverage. Hundreds of feeds across crypto, FX, commodities, and equities, so new markets are straightforward to add.
- Confidence intervals. Every price carries a confidence metric, which the protocol uses to reject unusually uncertain prices.
- Proven. Pyth is used across major Solana DeFi protocols.
How prices are sampled#
Seesaw uses Sampling Rule A: the first valid price at or after a boundary, enforced on-chain via the prev_publish_time witness field. The start price is captured atomically inside CreateMarket; the end price by SnapshotEnd. Both snapshots are write-once. The mechanism is described in full in Oracle Integration.
The rationale for choosing Sampling Rule A over alternatives (e.g., TWAP, time-averaged, or closing price):
- A TWAP is manipulable with large trades during the averaging window.
- A "closing price" is ambiguous on a 24/7 oracle stream.
- The first post-boundary print is the most transparent and reproducible rule for a permissionless system: any observer can independently verify which print qualifies, and the outcome cannot change once the qualifying print is posted.
Two oracle modes: push and pull#
Markets are created in one of two Pyth integration modes, chosen at creation
via CreateMarketArgs.oracle_mode. The mode is immutable after creation.
Push mode (oracle_mode = 0, default)#
- The market stores the address of the Pyth feed account as
market.pyth_feed(immutable after creation). - At
SnapshotEnd/ExpireMarket, the program verifies:- the supplied account address matches
market.pyth_feed, and - the account is owned by
config.pyth_receiver_program_id(sponsored push feeds are receiver-owned PDAs;config.pyth_program_idis used only to derive the push feed PDA address, not as the owner).
- the supplied account address matches
- A pull update for the same asset is rejected — the address binding rejects it before price data is read.
Trade-off: push reads the latest price in the persistent account. By the
time a crank reads it, an earlier valid post-boundary sample may already exist,
causing the prev_publish_time < boundary firstness check to fail and pushing
the market toward the Expired (50/50) fallback.
Pull mode (oracle_mode = 1)#
- The market stores
pyth_feed = [0u8; 32]as the pull indicator (no address). pyth_feed_idstores the 32-byte asset identifier (must be non-zero).- At each oracle read site, the caller provides the current ephemeral
PriceUpdateV2produced by the Pyth Receiver program after a HermesgetPriceUpdatesAtTimestamp(boundary, feed)call. Validation:feed_idmust matchmarket.pyth_feed_id, and- account owner must be
config.pyth_receiver_program_id. verification_level >= Full (1)is enforced by the price reader.
Advantage: Hermes can serve exactly the first post-boundary print, so
prev_publish_time < boundary is satisfied deterministically. Pull mode fits
Sampling Rule A with no ambiguity.
How the program routes validation#
At every oracle read site (CreateMarket, SnapshotEnd, ExpireMarket), the program tests whether market.pyth_feed is the all-zeros pull indicator: if so it validates feed-id and Receiver ownership; otherwise it validates the stored feed address, feed-id, and push-feed ownership. The complete validation logic is described in Oracle Integration.
Staleness and freshness bounds#
Start and end snapshots use different freshness bounds:
| Snapshot | Instruction | Freshness bound |
|---|---|---|
| Start | CreateMarket (0x03) | Fixed — hardcoded 60 s (MAX_PRICE_AGE_SECONDS) |
| End | SnapshotEnd (0x04) / late-capture | Configurable — config.max_price_staleness_seconds (via UpdateOperationalParams 0x2F) |
Integrators must not assume these are equal: an max_price_staleness_seconds
other than 60 makes the end snapshot more or less tolerant than the start.
How prices are validated#
The oracle validation layer defends against price manipulation, feed substitution, stale data, and future-dated prices. The complete validation mechanics are described in Oracle Integration.
The design principle is defense-in-depth without a Pyth SDK: the program parses PriceUpdateV2 at fixed byte offsets and applies every guard at read time, not at settlement time. This means a corrupted or wrong account never reaches the snapshot store — snapshots are immutable once captured and idempotent to capture, so any crank can safely retry without risking an overwrite.
Governance re-point at the 2026-07-31 Pyth cutover#
Pyth is upgrading its Core programs on 2026-07-31. The push feed PDA derivation program and the Receiver owner program must be updated before the cutover to keep new markets working.
Seesaw handles this via a two-step on-chain governance flow requiring no redeploy:
target = 0— re-pointsconfig.pyth_program_id(push feed PDA derivation program).target = 1— re-pointsconfig.pyth_receiver_program_id.- The 48-hour (
ORACLE_TIMELOCK_SECONDS) timelock allows the community to observe and react to the pending change before it takes effect. - Markets already in flight at the cutover date are unaffected — their oracle reads use the config value at snapshot time.
Expired markets and oracle outages#
If Pyth cannot supply a usable post-boundary price by the end of the
expiration window (default 7 days after t_end, configurable via
UpdateOperationalParams), ExpireMarket (0x06) transitions the market to
the Expired state. Both YES and NO shares then redeem at 0.50 USDT — no
funds are stranded by an oracle outage.
The expiration instruction first attempts a late capture with the current Pyth price (same Rule A firstness and boundary requirements). Only if that attempt fails does it fall through to the 50/50 path.
Alternatives we did not take#
- Multiple oracles aggregated together adds inconsistency between sources and reintroduces bridge risk for non-native oracles.
- A custom on-chain TWAP is manipulable with large trades and depends on deep local liquidity.
- Chainlink via a bridge adds trust assumptions, latency, and extra failure modes.
- Dispute windows delay settlement and create a denial-of-service surface, incompatible with Seesaw's instant, deterministic settlement.
The trade-off of a single oracle is a hard dependency on Pyth: if Pyth stops
publishing for a feed, markets on that feed fall back to the 50/50 Expired
path rather than resolving UP/DOWN.
See Oracle Integration for the full mechanism description — push/pull validation sequences, sampling rule enforcement, error codes, and the 2026 cutover program IDs.