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.
Pull-only oracle integration#
Markets use a Receiver-backed PriceUpdateV2 account. There is no oracle-mode
selector or Push compatibility branch.
Pull update account#
- 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 validates the supplied feed ID, Receiver ownership, verification level,
freshness, and firstness witness. 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.
Pyth program IDs are frozen#
The Pyth Core cutover is reflected in source as a hard-pinned Receiver owner
program ID. The historical 0x30/0x31 assignments
are reserved wire holes; default dispatch rejects either transaction with
InvalidInstructionData before payload decoding.
Operationally, there is no live two-step Pyth owner-program re-point. If Pyth changes these owner programs again, Seesaw must treat it as a program-upgrade event with audit/release evidence, not as a normal config proposal.
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 sampling rules, validation sequences, error codes, and the 2026 Receiver cutover.