Reading Data#
How to fetch and decode market, order book, and position data from Seesaw.
There are two read paths, matching the two SDK families:
- Trust-based — the
@seesaw/coreAPI client and WebSocket streams read from Seesaw's hosted indexer. One HTTP call for aggregated, paginated data. - Trustless —
@seesaw/trustlessreads raw accounts from any standard Solana RPC node, validates each one (owner, discriminator, size, PDA re-derivation), and decodes with@seesaw/corecodecs. Every read is slot-anchored.
Both paths use the same Kit v6 types: addresses are Address strings,
quantities are bigint (6-decimal base units), prices are number basis
points in [0, 10000].
Deriving PDAs#
All account addresses derive deterministically. The helpers are async and
return [address, bump]:
import {
deriveConfigPda,
deriveMarketPda,
deriveOrderbookPda,
deriveVaultPda,
derivePositionPda,
deriveTraderLedgerPda,
getCurrentMarketId,
} from '@seesaw/core';
import { address } from '@solana/addresses';
// Config (singleton)
const [configPda] = await deriveConfigPda();
// Market PDA — seeds: pyth_feed_id (32 bytes), duration_seconds, market_id, creator
const pythFeedId = new Uint8Array(32); // the market's 32-byte Pyth feed id
const durationSeconds = 900n;
const marketId = getCurrentMarketId(900); // floor(unix_now / duration)
const creator = address('CREATOR_WALLET');
const [marketPda] = await deriveMarketPda(pythFeedId, durationSeconds, marketId, creator);
// Market-scoped accounts
const [orderbookPda] = await deriveOrderbookPda(marketPda);
const [vaultPda] = await deriveVaultPda(marketPda);
const [traderLedgerPda] = await deriveTraderLedgerPda(marketPda);
// User position
const user = address('USER_WALLET');
const [positionPda] = await derivePositionPda(marketPda, user);
Market cadence helpers (market_id = floor(unix_timestamp / duration_seconds)):
import { getMarketId, getCurrentMarketId, getMarketTimeRange } from '@seesaw/core';
const id = getMarketId(Math.floor(Date.now() / 1000), 900); // explicit timestamp
const { start, end } = getMarketTimeRange(id, 900); // Date objects
In practice you rarely derive market PDAs by hand: the API returns market addresses, and
listMarkets(trustless) enumerates them from chain state.
Decoding Accounts#
@seesaw/core exports a codec per account type: decodeConfigAccount,
decodeMarketAccount, decodeOrderbookAccount, decodePositionAccount,
decodeAssetState. They take raw account bytes (Uint8Array) from any RPC:
import { createSolanaRpc } from '@solana/rpc';
import type { Address } from '@solana/addresses';
import { decodeMarketAccount } from '@seesaw/core';
const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL!);
async function fetchAccountData(account: Address): Promise<Uint8Array | null> {
const result = await rpc
.getAccountInfo(account, { encoding: 'base64', commitment: 'confirmed' })
.send();
if (!result.value) return null;
const data = result.value.data;
return Array.isArray(data) && typeof data[0] === 'string'
? new Uint8Array(Buffer.from(data[0], 'base64'))
: null;
}
const bytes = await fetchAccountData(marketPda);
if (bytes) {
const market = decodeMarketAccount(bytes);
console.log(market.marketId, market.tStart, market.tEnd, market.outcome);
}
If you use @seesaw/trustless, you usually don't decode by hand — the
resolver fetches, validates, and decodes in one call (next section).
Reading via the Trustless Resolver#
Every resolver read returns { slot, value } — the slot anchors the snapshot
the data was evaluated at. Failures throw typed errors
(AccountNotFoundError, AccountValidationError, DecodeError, …); there
are no silent fallbacks.
import { TrustlessRpc, TrustlessResolver, listMarkets, listUserPositions } from '@seesaw/trustless';
import { address } from '@solana/addresses';
const rpc = new TrustlessRpc({ url: process.env.SOLANA_RPC_URL! });
const resolver = new TrustlessResolver(rpc);
const user = address('USER_WALLET');
// ── Discovery (getProgramAccounts; no indexer) ────────────────────────────
const markets = await listMarkets(rpc); // [{ address, market }]
const positions = await listUserPositions(rpc, user); // [{ address, position }]
// ── Validated single reads ────────────────────────────────────────────────
const marketAddress = markets[0].address;
const { value: market, slot } = await resolver.getMarket(marketAddress);
const { value: book } = await resolver.getOrderbook(marketAddress);
const { value: position } = await resolver.getPosition(marketAddress, user); // null if none
// Free/locked balance buckets from the trader ledger:
const { value: balances } = await resolver.getTraderBalances(marketAddress, user);
// The user's resting orders, decoded from the on-chain book
// (the trustless replacement for the indexer's order list):
const { value: orders } = await resolver.findUserOrders(marketAddress, user);
// → [{ orderId, priceBps, quantity, originalSide, isBid }]
// Referral status, entirely on-chain:
const now = await resolver.getChainUnixTimestamp();
const referral = await resolver.getReferralStatus(user, now); // 'none' | 'expired' | 'active'
Validation performed before any value is returned: program ownership, account discriminator, size, and — for markets — a PDA cross-check: the market address must re-derive from its own decoded seed fields. Spoofed but well-formed accounts are rejected. See the Trustless SDK guide for the full safeguard inventory.
Reading via the Hosted API#
import { createSeesawClient } from '@seesaw/core';
const client = createSeesawClient({ apiUrl: 'https://api.seesaw.markets' });
// Markets
const markets = await client.api.markets.list({ limit: 20 });
const current = await client.api.markets.current();
const one = await client.api.markets.get('MARKET_ID');
// Orders & positions (authenticated routes — see the API reference)
const orders = await client.api.orders.list({ status: 'open' });
const positions = await client.api.positions.list({ owner: 'USER_WALLET', settled: false });
// Protocol fee curve parameters (cap + decay) for fill estimation
const feeConfig = await client.api.fees.getConfig();
Standalone fetchers are also exported (fetchMarkets, fetchActiveMarkets,
fetchMarketDetail, fetchPositions, fetchPendingOrders,
fetchTradeHistory, fetchLeaderboard, …) if you don't want the unified
client. See the API Reference for routes and
response shapes.
Live updates (WebSocket streams)#
const stopBook = client.streams.orderbook('MARKET_ADDRESS', (update) => {
console.log('book update', update.data);
});
const stopMarket = client.streams.market('MARKET_ADDRESS', (update) => {
console.log('market update', update.data);
});
// Authenticated position updates: client.streams.positions(wallet, auth, cb)
// Each subscription returns an unsubscribe handle.
For a trustless alternative, subscribe to the market/orderbook accounts with
your own RPC's standard accountSubscribe and decode the payload with
decodeMarketAccount / decodeOrderbookAccount.
Market State#
The decoded MarketAccount exposes lifecycle fields (tStart, tEnd,
startPrice, endPrice, outcome, emergencyStatus, …). Helpers derive
display state:
import {
getMarketState,
isMarketTradable,
getMarketProgress,
MarketState,
Outcome,
} from '@seesaw/core';
const state = getMarketState(market); // Pending | Created | Trading | Settling | Resolved | Closed
const tradable = isMarketTradable(market);
const progress = getMarketProgress(market.marketId, Number(market.durationSeconds)); // 0..1
enum MarketState {
Pending = 0,
Created = 1,
Trading = 2,
Settling = 3,
Resolved = 4,
Closed = 5,
}
enum Outcome {
None = 0,
Up = 1, // P_end >= P_start (equality resolves UP)
Down = 2,
Expired = 3, // oracle unavailable → 50/50 settlement
}
Order Book#
The on-chain order book is a single canonical YES book; NO orders appear at
the complement price. The decoded OrderbookAccount has fixed-size
bids/asks arrays where inactive slots have isActive = false — always go
through the SDK helpers, which filter and sort correctly:
import {
aggregateOrderbook,
getBestBid,
getBestAsk,
getSpread,
getMidPrice,
getOrdersByOwner,
} from '@seesaw/core';
const { value: orderbook } = await resolver.getOrderbook(marketAddress);
const levels = aggregateOrderbook(orderbook);
// levels.bids: OrderbookLevel[] sorted price DESC (best bid first)
// levels.asks: OrderbookLevel[] sorted price ASC (best ask first)
// OrderbookLevel = { price: number (bps), quantity: bigint, orders: number }
const bestBid = getBestBid(orderbook); // number | null
const bestAsk = getBestAsk(orderbook); // number | null
const spread = getSpread(orderbook); // number | null
const mid = getMidPrice(orderbook); // number | null (floor of (bid+ask)/2)
const mine = getOrdersByOwner(orderbook, user); // Order[]
Estimating Fills and Slippage#
Web and mobile use one shared estimator, exported from @seesaw/core:
estimateFillFromLevels. It walks the aggregated book for a prospective
taker order and returns a UI-ready FillEstimate — and it is canonically
correct for NO-side orders (it maps them through the complement price and
returns all prices in your denomination).
import {
aggregateOrderbook,
estimateFillFromLevels,
decodeConfigAccount,
deriveConfigPda,
OrderSide,
OrderType,
} from '@seesaw/core';
import { TrustlessRpc, TrustlessResolver } from '@seesaw/trustless';
const rpc = new TrustlessRpc({ url: process.env.SOLANA_RPC_URL! });
const resolver = new TrustlessResolver(rpc);
// 1. A current book snapshot, aggregated into price levels.
const { value: orderbook } = await resolver.getOrderbook(marketAddress);
const book = aggregateOrderbook(orderbook);
// 2. The live fee curve parameters from the on-chain config.
const {
value: { config },
} = await resolver.getConfig();
// 3. Estimate: buy 50 NO shares (50_000_000 base units) at up to 42.00%.
const estimate = estimateFillFromLevels({
side: OrderSide.BuyNo,
quantity: 50_000_000n,
orderType: OrderType.Limit,
limitPriceBps: 4_200, // NO price — caller denomination
book,
fee: { capBps: config.feeCapBps, decayBps: config.decayRateBps },
});
console.log({
filled: estimate.filled, // fills immediately
remaining: estimate.remaining, // blocked by price bound / thin book
restsRemainder: estimate.restsRemainder, // true → leftover rests as a maker order
avgPriceBps: estimate.avgPriceBps, // size-weighted average (NO denomination here)
worstPriceBps: estimate.worstPriceBps, // deepest level touched
refPriceBps: estimate.refPriceBps, // best opposing level before the order
priceImpactBps: estimate.priceImpactBps, // worst vs ref
slippageBps: estimate.slippageBps, // avg vs ref
cost: estimate.cost, // pre-fee notional
feeAmount: estimate.feeAmount, // taker fee (0 if no fee config passed)
totalCost: estimate.totalCost, // buys: cost + fee; sells: cost − fee
});
FillEstimate fields:
| Field | Meaning |
|---|---|
filled / remaining | Base units that fill now vs. don't |
restsRemainder | true when the leftover will rest on the book (Limit/PostOnly); false for IOC |
avgPriceBps | Size-weighted average fill price, caller denomination |
worstPriceBps | Deepest level touched (worst price for the taker) |
refPriceBps | Best opposing level before the order |
priceImpactBps | worstPriceBps vs refPriceBps, non-negative |
slippageBps | avgPriceBps vs refPriceBps, non-negative |
cost / feeAmount / totalCost | Pre-fee notional, estimated taker fee, and the total |
Two optional knobs:
worstAcceptablePriceBps— mirrors the on-chain IOC slippage bound; the estimate respects it the way the program will.fee.split— override the protocol/creator/referral fee split (defaults to the protocol'sDEFAULT_FEE_SPLIT).
Estimates, not settlement. The estimator reads a possibly-stale snapshot
and computes the fee once on the average price. The binding on-chain
protections are your order's limit price and (for IOC) the
worst_acceptable_price_bps argument. For the full mechanics, see
Slippage & fill estimation.
A simpler market-order walker, estimateFill(side, quantity, orderbook),
also exists for quick canonical-side estimates without fee/impact breakdown.
Positions#
The decoded UserPositionAccount:
const { value: position } = await resolver.getPosition(marketAddress, user);
if (position) {
const availableYes = position.yesShares - position.lockedYesShares;
const availableNo = position.noShares - position.lockedNoShares;
console.log({
availableYes,
availableNo,
collateralDeposited: position.collateralDeposited,
collateralLocked: position.collateralLocked, // resting-bid collateral
settled: position.settled,
payout: position.payout,
totalFeesPaid: position.totalFeesPaid,
orderCount: position.orderCount,
});
}
Locked collateral is not auto-returned. Resting limit-buy collateral stays locked until you cancel the order, the order fills, or you redeem after resolution. Claim promptly after resolution — see Settlement for the teardown timeline.
Price Display Helpers#
import {
bpsToPercent,
formatPrice,
complementPrice,
impliedProbability,
formatQuantity,
} from '@seesaw/core';
formatPrice(6_500); // "65.00%"
complementPrice(6_500); // 3500 — the NO price for a 65% YES price
formatQuantity(1_500_000n); // formats 6-decimal base units
Next Steps#
- Learn Building Transactions
- See complete Examples
- Go deeper with the Trustless SDK