Trustless SDK#
Trade, cancel, redeem, and read Seesaw state with nothing but a standard Solana RPC node — no Seesaw-operated service in the trust path.
The trustless SDKs ship in three languages:
| Language | Package | Source | Builds on |
|---|---|---|---|
| TypeScript | @seesaw/trustless | packages/trustless | @seesaw/core (encoders, PDAs) |
| Python | seesaw-trustless | packages/trustless-python | seesaw-sdk (encoders, PDAs) |
| Rust | seesaw-trustless | packages/trustless-rust | seesaw-sdk crate (encoders, PDAs) |
All three are ports of the same design: the trust model, safeguards, and
validation semantics are identical, and they are pinned to each other by
shared golden test vectors (packages/test-vectors).
Why Trustless?#
The trust-based SDKs read markets, orders, and positions from Seesaw's hosted indexer/API. That is convenient — one HTTP call returns aggregated, paginated data — but it puts a Seesaw-operated service in your read path. The trustless SDKs invert that relationship: the indexer becomes a convenience cache, not a trust dependency. Every account an instruction needs is derived, discovered, and validated directly from chain state, with the same checks the on-chain program itself applies.
Given a user's intent ("place an order on market M", "cancel my order", "redeem my winnings"), the resolver derives or discovers every account the instruction needs, validates each one, and hands the result to the canonical instruction builders. If Seesaw's entire infrastructure disappeared tomorrow, a user with this package and any RPC endpoint could still trade, cancel, redeem, claim, and exit.
Trust Model Comparison#
| Component | Trust-based SDKs | Trustless SDKs |
|---|---|---|
| Seesaw indexer / API | Trusted for reads (market lists, orders, positions) | Never used — no code path touches it |
| Your RPC node | Used for submission | Trusted for data availability only: every value-bearing account is validated, not trusted. A lying node can deny service; it cannot make the SDK build a transaction the program would not itself validate |
Encoding layer (@seesaw/core / seesaw-sdk) | Trusted | Trusted — canonical builders held byte-identical to the program by CI golden vectors (packages/test-vectors) |
| The on-chain program | Final arbiter | Final arbiter — re-validates everything (see the on-chain security model) |
| Your keys | Never touched by the SDK | Never touched by the SDK — it prepares; your wallet decides |
The Safeguards#
Every trustless SDK implements the same ten safeguards:
- Fail-closed everywhere. Every resolution failure surfaces as a typed
error —
AccountNotFoundError,AccountValidationError,PreconditionError,DecodeError,NotFoundOnChainError,RpcTransportError,SimulationFailedError(TypeScript and Python; the Rust crate uses the matchingTrustlessErrorvariants). There are no fallback values, no "proceed anyway" paths, no silently skipped accounts. - On-chain validation mirrored client-side. Program ownership, account discriminator, size, layout version, and mint/authority pinning — the same checks the program applies before moving funds — run on every fetched account before a transaction is constructed.
- PDA cross-checks beyond ownership. A market account must not only be program-owned with the right discriminator — its address must re-derive from its own decoded seed fields (feed id, duration, market id, creator), and its stored YES/NO mints must match their canonical PDA derivations. Spoofed-but-well-formed accounts are rejected.
- Treasury index/account pairing solved structurally. The protocol-fee
recipient is read from the on-chain config at the picked index
(
pickTreasuryIndex/pick_treasury_index) and validated as a live, unfrozen token account of the settlement mint. The index/account mismatch class that exists when callers pass both values by hand cannot occur. - Order ids verified on-chain before cancel/reduce.
requireUserOrder(require_user_order) confirms the id exists on the on-chain book and belongs to the user before an instruction is built — the trustless replacement for trusting an API's order list. - Slot-anchored reads. Every resolution reports the slot it was
evaluated at; multi-account reads are batched into single
getMultipleAccountscalls (always ≤ 100 accounts) so each batch is a consistent snapshot. - Precondition mirrors, clearly labeled. Pause, market emergency status, trading window, referral expiry, and resolved-market gates are checked client-side to fail fast — and they are mirrors: the program's own gates remain the enforcement. A stale mirror can only produce an on-chain rejection, never a wrong execution.
- Pre-flight simulation support.
simulateTransaction(simulate_transaction) runs withreplaceRecentBlockhashandsigVerify: false, so the fully assembled transaction can be simulated before the user signs; simulation failures carry the program logs. - Missing ATAs handled explicitly. Resolutions return idempotent
CreateIdempotentATA instructions for any of the user's token accounts that do not exist yet — never a substituted account, never a silent skip. - No silent partial results in discovery.
listMarkets/listUserPositionsdecode every returned account and fail on the first malformed one rather than presenting a partial list as complete.
RPC Compatibility#
Works against any endpoint implementing the standard Solana JSON-RPC API
(getAccountInfo, getMultipleAccounts, getProgramAccounts,
getLatestBlockhash, simulateTransaction). Vendor accelerators are
optional and transparent — they change transport efficiency, never results:
- Helius — the paginated
getProgramAccountsV2extension is probed automatically and used when available; a-32601response falls back to standardgetProgramAccounts. Modes:auto(default) /never/always. - Triton — zero configuration: Steamboat accelerates the standard
getProgramAccountstransparently on Triton endpoints.
Using the Trustless Client#
The flow is always: resolve → build → simulate → sign & send. The examples below are TypeScript; for Python and Rust, see the dedicated language pages.
TypeScript: Install#
npm install @seesaw/trustless @seesaw/core
(See Installation for monorepo paths until registry publication lands.)
TypeScript: Discover markets and place an order#
The resolver returns validated accounts plus any prerequisite ATA-create
instructions; the @seesaw/core builder produces the Kit Instruction;
your wallet stack does the rest.
import {
TrustlessRpc,
TrustlessResolver,
listMarkets,
TOKEN_PROGRAM_ADDRESS,
SYSTEM_PROGRAM_ADDRESS,
} from '@seesaw/trustless';
import { buildPlaceOrderIx, OrderSide, OrderType } from '@seesaw/core';
import { address } from '@solana/addresses';
const rpc = new TrustlessRpc({
url: process.env.SOLANA_RPC_URL!, // any standard Solana RPC
// headers: { Authorization: `Bearer ${apiKey}` }, // optional provider auth
// commitment: 'confirmed', // default
// gpaV2: 'auto', // Helius pagination probe (default)
});
const resolver = new TrustlessResolver(rpc);
const user = address('YOUR_WALLET');
// 1. Discover every live market straight from chain state (no Seesaw API).
const markets = await listMarkets(rpc); // [{ address, market }]
const marketAddress = markets[0].address;
// 2. Resolve everything PlaceOrder needs — validated, slot-anchored.
// Throws a typed error (never a fallback) on any validation failure.
const r = await resolver.resolvePlaceOrder({ marketAddress, user });
// r.accounts — full validated account set (incl. config + treasury shard)
// r.protocolTreasuryIndex — structural pick, validated against on-chain config
// r.referral — { state: 'none' | 'expired' | 'active', accounts? }
// r.prependInstructions — idempotent ATA creates for missing token accounts
// r.state — decoded config + market (read at r.slot)
// r.slot — the slot this resolution was evaluated at
// 3. Build the instruction with the canonical @seesaw/core builder.
const ix = buildPlaceOrderIx(
{
side: OrderSide.BuyYes,
priceBps: 6_500, // 65%
quantity: 1_000_000n, // base units (6 decimals)
orderType: OrderType.Limit,
protocolTreasuryIndex: r.protocolTreasuryIndex,
},
{
...r.accounts,
user,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
...(r.referral.state === 'active' && r.referral.accounts
? {
takerReferralAccount: r.referral.accounts.referralAccount,
referrerEarningsAccount: r.referral.accounts.referrerEarningsAccount,
referrerTreasury: r.referral.accounts.referrerTreasury,
}
: {}),
}
);
// 4. Assemble [...r.prependInstructions, ix] into one transaction with your
// Kit signing stack, then simulate before the user signs:
// const sim = await rpc.simulateTransaction(base64EncodedTx);
// if (sim.err) throw new Error(sim.logs.join('\n'));
// ...sign and send. See ./building-transactions.md for the full flow.
TypeScript: Cancel an order#
// Find your resting orders by decoding 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 }]
const orderId = orders[0].orderId;
// Fail-closed existence check: throws NotFoundOnChainError unless this id
// is active on the book AND owned by `user`.
await resolver.requireUserOrder(marketAddress, user, orderId);
// Resolve the shared trading account set (validated, with missing-ATA fixes).
import { buildCancelOrderIx } from '@seesaw/core';
const { value: t } = await resolver.resolveTradingAccounts({ marketAddress, user });
const { prependInstructions, ...accounts } = t;
const cancelIx = buildCancelOrderIx(
{ orderId },
{ ...accounts, user, tokenProgram: TOKEN_PROGRAM_ADDRESS }
);
// Assemble [...prependInstructions, cancelIx], simulate, sign, send.
TypeScript: Redeem after resolution#
import { buildRedeemIx, TokenType } from '@seesaw/core';
const { value: t } = await resolver.resolveTradingAccounts({ marketAddress, user });
const redeemIx = buildRedeemIx(
{ amount: 1_000_000n, tokenType: TokenType.Yes }, // burn winning shares
{
market: t.market,
yesMint: t.yesMint,
noMint: t.noMint,
userYesAta: t.userYesAta,
userNoAta: t.userNoAta,
userStablecoinAta: t.userTokenAccount,
vault: t.vault,
user,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
settlementMint: t.settlementMint,
// Optional but recommended: lets Redeem settle the position and release
// resting-bid collateral / locked maker claims in the same call.
userPosition: t.userPosition,
orderbook: t.orderbook,
traderLedger: t.traderLedger,
}
);
Claim promptly. Resting limit-buy collateral is not auto-returned, and unclaimed funds are eventually swept when the market is torn down. See Settlement for the exact timeline.
TypeScript: Read APIs#
Every read returns { slot, value } — the slot anchors the snapshot the data
was evaluated at.
// Validated single reads (owner + discriminator + size + PDA cross-checks)
const { value: market } = await resolver.getMarket(marketAddress);
const { value: book } = await resolver.getOrderbook(marketAddress);
const {
value: { config },
} = await resolver.getConfig();
// Position (null if none exists)
const { value: position } = await resolver.getPosition(marketAddress, user);
// Free/locked balance buckets from the trader ledger
const { value: balances } = await resolver.getTraderBalances(marketAddress, user);
// Resting orders decoded from the on-chain book
const { value: orders } = await resolver.findUserOrders(marketAddress, user);
// Referral status, entirely on-chain (binding PDA + 365-day expiry +
// referrer earnings account + bound treasury shard)
const now = await resolver.getChainUnixTimestamp(); // Clock sysvar
const referral = await resolver.getReferralStatus(user, now);
// → { state: 'none' | 'expired' | 'active', referrer?, accounts? }
// Chain-wide discovery (getProgramAccounts)
import { listMarkets, listUserPositions } from '@seesaw/trustless';
const allMarkets = await listMarkets(rpc);
const myPositions = await listUserPositions(rpc, user);
Python#
For the full Python trustless walkthrough (install, place order, cancel,
redeem, read APIs), see the Python SDK guide.
The API surface follows the same pattern as TypeScript with snake_case
method names (resolve_place_order, find_user_orders, etc.) and
solders-based account types.
Rust#
For the full Rust trustless walkthrough (install, place order, cancel,
redeem, read APIs), see the Rust SDK guide.
The API surface follows the same pattern as TypeScript with snake_case
method names and the TrustlessError enum for typed failures.
Resolver API Reference#
The same surface in all three languages (camelCase in TypeScript, snake_case in Python/Rust). Every read returns the value with the slot it was evaluated at.
| Method | What it does |
|---|---|
getConfig() | Fetch + validate the protocol config (owner, discriminator, size) |
getMarket(market) | Fetch + validate a market, including the PDA re-derivation cross-check from its own decoded seed fields |
getOrderbook(market) | Fetch + validate the order book |
getPosition(market, user) | Fetch + validate the user's position; null/None if absent |
getTraderBalances(market, user) | The user's free/locked balance buckets from the trader ledger |
findUserOrders(market, user) | The user's resting orders decoded from the on-chain book — drives cancel/reduce flows |
requireUserOrder(market, user, orderId) | Fail-closed check that an order id exists and belongs to the user |
getReferralStatus(user, nowUnix) | Referral binding, 365-day expiry, earnings account, bound treasury shard — entirely on-chain |
getChainUnixTimestamp() | On-chain unix time from the Clock sysvar (for trading-window mirrors) |
resolvePlaceOrder({market, user}) | Everything PlaceOrder (and its free-funds variants) needs: validated accounts, structural treasury pick, referral status, missing-ATA instructions, precondition mirrors |
resolveTradingAccounts({market, user}) | The shared trading account set for MintShares, Deposit/WithdrawFunds, WithdrawShares, Cancel/Reduce, and Redeem |
listMarkets(rpc) | Chain-wide market discovery via getProgramAccounts (size + discriminator filters) |
listUserPositions(rpc, user) | All of a user's positions across markets (memcmp on the owner field) |
Typed errors#
| Error | Meaning |
|---|---|
RpcTransportError | Transport, HTTP, or JSON-RPC level failure |
AccountNotFoundError | A required account does not exist on-chain |
AccountValidationError | Owner / discriminator / size / PDA cross-check / mint / authority pinning failed |
PreconditionError | A client-side mirror of an on-chain gate failed fast (pause, emergency status, trading window, resolved market, treasury index bounds) |
DecodeError | An account decoded inconsistently with the expected layout |
NotFoundOnChainError | A logical entity (e.g. an order id) is not present on-chain |
SimulationFailedError | Pre-flight simulation failed; carries the program logs |
(The Rust crate models these as variants of the TrustlessError enum:
RpcTransport, AccountNotFound, AccountValidation, Precondition,
Decode, NotFoundOnChain, SimulationFailed.)
Scope and Roadmap#
Covered now: config/market/orderbook/position/trader-ledger/referral reads with validation; market and user-position discovery; full PlaceOrder resolution (treasury, referral triple, missing-ATA handling, precondition mirrors); the shared trading-account set for MintShares, Deposit/Withdraw, WithdrawShares, Cancel/Reduce, and Redeem; on-chain order lookup for cancel/reduce.
Planned: resolution sets for the permissionless lifecycle cranks
(SnapshotEnd/Resolve/Expire/CloseMarket) and reclaim; a streaming cache layer
over standard programSubscribe.
Related#
- SDK overview & family matrix
- Installation
- Reading Data — both read paths side by side
- Building Transactions — the Kit signing flow
- Flow of funds — where funds live at every step; the on-chain security model documents the validation gates this SDK mirrors
- Settlement & teardown timeline — why you should redeem promptly