Reading data#
The canonical v1 clients use generated account readers. Fetch finalized bytes
from your RPC provider, derive the expected PDA, and decode through
@seesaw/core; malformed or mismatched accounts fail closed.
Derive PDAs#
If you already know the complete market identity, derive its PDA directly. A timestamp and duration alone are insufficient: the creator is part of the native-market seed tuple. Discover candidate accounts first when that tuple is unknown.
// docs-check: semantic
import type { Address } from '@solana/addresses';
import {
deriveConfigPda,
deriveMarketPda,
deriveOrderbookPda,
derivePositionPda,
deriveTraderLedgerPda,
} from '@seesaw/core';
export async function deriveAccounts(
feedId: Uint8Array,
durationSeconds: bigint,
marketId: bigint,
creator: Address,
wallet: Address
) {
const [config] = await deriveConfigPda();
const [market] = await deriveMarketPda(feedId, durationSeconds, marketId, creator);
const [orderbook] = await deriveOrderbookPda(market);
const [position] = await derivePositionPda(market, wallet);
const [ledger] = await deriveTraderLedgerPda(market);
return { config, market, orderbook, position, ledger };
}
The complete seed and layout tables live in the account reference
and spec/ACCOUNTS.md. Never accept a caller-supplied account merely because
it is owned by the program; verify discriminator, exact size/version, PDA, and
related-market fields.
Discover native markets by creator#
This read-only recipe calls Solana getProgramAccounts, filters by the generated
market discriminator and exact market account size, and optionally filters the
creator field at byte offset 284. The response is still validated after filtering.
Market accounts have one fixed size; it is their orderbooks, not their market
accounts, that have per-capacity sizes.
// docs-check: semantic
import { createSolanaRpc } from '@solana/rpc';
import { getAddressEncoder, type Address } from '@solana/addresses';
import { getBase64Decoder, getBase64Encoder } from '@solana/codecs-strings';
import type { Base64EncodedBytes, GetProgramAccountsMemcmpFilter } from '@solana/rpc-types';
import {
MARKET_ACCOUNT_DISCRIMINATOR,
MARKET_ACCOUNT_SIZE,
decodeMarketAccount,
deriveMarketPda,
} from '@seesaw/core';
export async function discoverNativeMarkets(rpcUrl: string, program: Address, creator?: Address) {
const toBase64 = (bytes: Uint8Array): Base64EncodedBytes =>
getBase64Decoder().decode(bytes) as Base64EncodedBytes;
const creatorFilter: GetProgramAccountsMemcmpFilter[] = creator
? [
{
memcmp: {
offset: 284n,
bytes: toBase64(new Uint8Array(getAddressEncoder().encode(creator))),
encoding: 'base64',
},
},
]
: [];
const response = await createSolanaRpc(rpcUrl)
.getProgramAccounts(program, {
commitment: 'finalized',
encoding: 'base64',
withContext: true,
filters: [
{ dataSize: BigInt(MARKET_ACCOUNT_SIZE) },
{
memcmp: { offset: 0n, bytes: toBase64(MARKET_ACCOUNT_DISCRIMINATOR), encoding: 'base64' },
},
...creatorFilter,
],
})
.send();
const markets = [];
for (const { pubkey, account } of response.value) {
if (account.owner !== program) throw new Error('Unexpected account owner');
const market = decodeMarketAccount(new Uint8Array(getBase64Encoder().encode(account.data[0])));
if (market.marketKind !== 0) continue; // External identity uses MarketMeta, not native seeds.
const [expected, bump] = await deriveMarketPda(
market.pythFeedId,
market.durationSeconds,
market.marketId,
market.creator,
program
);
if (pubkey !== expected || market.bump !== bump || (creator && market.creator !== creator)) {
throw new Error('Market identity mismatch');
}
markets.push({ address: pubkey, market, slot: response.context.slot });
}
return markets;
}
RPC operators may limit program-wide scans; surface that failure rather than
returning an empty catalog. Persist discovered identities and refresh them with
account subscriptions or bounded scans. External markets require the
market_ext and market_meta identity checks in PDAs.
Find positions, orders, and free funds#
For positions, use POSITION_ACCOUNT_DISCRIMINATOR and POSITION_ACCOUNT_SIZE
with the same query pattern, plus a memcmp filter at owner offset 40 and,
optionally, market offset 8. Then decode each position and verify its canonical
PDA against both stored keys. These filters select candidates; they do not
replace ownership, layout, or PDA validation.
For an individual market, deriving the orderbook and ledger PDAs is cheaper than
a program-wide scan. Decode the entire supported book and filter bids and
asks by the order's full owner key. Read remaining quantity, orderId,
status, and maxAgeSeconds; an expired claimable record is not an active quote.
Order records are embedded in the book, so an account-level memcmp is not a
general query for all orders belonging to a wallet.
When enumerating orderbook accounts themselves, issue separate dataSize
queries for deepOrderbookAccountSize(capacity) at each supported capacity,
alongside DEEP_ORDERBOOK_ACCOUNT_DISCRIMINATOR. A single market-account size
filter will not find those books. Validate the decoded parent market and derived
book address before using any order.
Decode the trader ledger and find the slot whose owner equals the wallet. Its free quote/YES/NO buckets belong only to that market. See Monitoring for custody reconciliation and why ledger funds must not be double-counted with the position's accounting fields.
Decode generated accounts#
// docs-check: semantic
import type { Address } from '@solana/addresses';
import {
decodeConfigAccount,
decodeMarketAccount,
decodeDeepOrderbookAccount,
decodePositionAccount,
decodeTraderLedgerAccount,
} from '@seesaw/core';
export async function decodeAccounts(
accounts: {
config: Address;
market: Address;
orderbook: Address;
position: Address;
ledger: Address;
},
fetchFinalizedBytes: (address: Address) => Promise<Uint8Array>
) {
return {
config: decodeConfigAccount(await fetchFinalizedBytes(accounts.config)),
market: decodeMarketAccount(await fetchFinalizedBytes(accounts.market)),
orderbook: decodeDeepOrderbookAccount(await fetchFinalizedBytes(accounts.orderbook)),
position: decodePositionAccount(await fetchFinalizedBytes(accounts.position)),
ledger: decodeTraderLedgerAccount(await fetchFinalizedBytes(accounts.ledger)),
};
}
Pass the addresses returned by deriveAccounts into decodeAccounts.
fetchFinalizedBytes is an application adapter, not an SDK export: it must
reject missing accounts and unexpected owners before returning binary data.
Decoders validate bytes; callers must verify address and related-account identity.
Deep orderbooks use only the supported capacities 64, 128, 256, 512, 1024, 2048, 4096; the requested capacity must match both sides. Generated readers
also enforce reserved-byte zeroes, occupied-slot counts, and compatibility
version rules.
External markets and Reclaim#
For a market originated elsewhere, keep the source reference and provenance
with the Seesaw market identity. Use @seesaw/reclaim to canonicalize proof
wire and the Reclaim service to submit the registered-resolver lifecycle
transition. This is the same account/PDA surface as native markets; there is
no second reader family.
Commitment and freshness#
Use finalized commitment for user-facing balances and settlement decisions.
Whatever fetch helper you wrap, pass the commitment explicitly rather than
relying on a provider default:
// docs-check: semantic
import { createSolanaRpc } from '@solana/rpc';
import type { Address } from '@solana/addresses';
export async function fetchFinalizedAccount(rpcUrl: string, market: Address) {
const rpc = createSolanaRpc(rpcUrl);
const { value } = await rpc
.getAccountInfo(market, { encoding: 'base64', commitment: 'finalized' })
.send();
return value; // Check owner and decode value.data before passing bytes to a decoder.
}
For live orderbook displays a lower commitment may be used, but only with an explicit slot and staleness indicator. Never mix data from different slots without recording the slot identity in the projection.
Estimating fills and slippage#
Use the generated deep-orderbook reader to obtain the current bid/ask ladder, then apply the same checked arithmetic as the on-chain matcher. Treat the result as a quote, not a settlement guarantee: a later slot can change the book before a signed transaction lands.
Cross-language parity#
The Rust and Python SDKs consume the same account and instruction vectors in
packages/test-vectors. Run pnpm --filter @seesaw/test-vectors test and the
language-specific test suites before publishing a client.