Building Transactions#
How to build, simulate, and send Seesaw transactions with Solana Kit v6.
Seesaw's TypeScript SDKs (@seesaw/core and @seesaw/trustless) produce
Solana Kit v6 Instruction values. Neither SDK ever signs or submits — that is
always the caller's wallet stack. This page covers:
- The Kit v6 transaction assembly pattern (compile + simulate + sign + send)
- Place, cancel, and redeem — Trust-based and Trustless side by side
- Market creation, including the
EnsureTraderLedgerSpaceprelude - Free-funds order variants
- Priority fees, compute budget, and retry guidance
Kit v6, not web3.js. Addresses are Kit
Addressstrings. Use@solana/transaction-messages,@solana/transactions, and@solana/keysfor assembly and signing. Do not mix in@solana/web3.jsobjects (PublicKey,Transaction).
Transaction Assembly Pattern#
Every Seesaw transaction follows the same Kit v6 pattern:
Minimal helper#
The examples below share this helper to keep the assembly boilerplate in one place. Bring your own signing stack — substitute a wallet adapter in browser contexts.
// docs-check: fragment reason: shared transaction setup
import { createKeyPairFromBytes } from '@solana/keys';
import { getAddressFromPublicKey } from '@solana/addresses';
import { pipe } from '@solana/functional';
import {
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
} from '@solana/transaction-messages';
import {
compileTransaction,
getBase64EncodedWireTransaction,
signTransaction,
} from '@solana/transactions';
import { createSolanaRpc } from '@solana/rpc';
import type { Instruction } from '@solana/instructions';
import { readFileSync } from 'node:fs';
/** Load a Solana CLI keypair file as a Kit CryptoKeyPair. */
export async function loadKeypair(path: string) {
const bytes = new Uint8Array(JSON.parse(readFileSync(path, 'utf-8')));
return createKeyPairFromBytes(bytes); // expects 64 bytes
}
/**
* Assemble, simulate, sign, and send a list of Kit Instructions.
* Returns the base58 transaction signature.
*/
export async function sendInstructions(
instructions: Instruction[],
keyPair: CryptoKeyPair,
rpcUrl: string
): Promise<string> {
const rpc = createSolanaRpc(rpcUrl);
const walletAddress = await getAddressFromPublicKey(keyPair.publicKey);
// 1. Fresh blockhash (lifetime anchor)
const {
value: { blockhash, lastValidBlockHeight },
} = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
// 2. Assemble the transaction message
const msg = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayer(walletAddress, m),
(m) => setTransactionMessageLifetimeUsingBlockhash({ blockhash, lastValidBlockHeight }, m),
(m) => appendTransactionMessageInstructions(instructions, m)
);
// 3. Compile to a wire-ready transaction, then simulate before signing
const compiled = compileTransaction(msg);
const base64Tx = getBase64EncodedWireTransaction(compiled);
const simResult = await rpc
.simulateTransaction(base64Tx, {
encoding: 'base64',
commitment: 'finalized',
replaceRecentBlockhash: true,
sigVerify: false,
})
.send();
if (simResult.value.err) {
throw new Error(
`Simulation failed: ${JSON.stringify(simResult.value.err)}\n` +
(simResult.value.logs ?? []).join('\n')
);
}
// 4. Sign and send
const signed = await signTransaction([keyPair], compiled);
const signedBase64 = getBase64EncodedWireTransaction(signed);
const sig = await rpc
.sendTransaction(signedBase64, {
encoding: 'base64',
preflightCommitment: 'finalized',
})
.send();
// 5. Confirm
await rpc
.confirmTransaction(sig, {
commitment: 'finalized',
strategy: { type: 'blockhash', blockhash, lastValidBlockHeight },
})
.send();
return sig;
}
Note on
pipe:pipeis a tiny functional-composition helper from@solana/functional— install it alongside the other Kit packages.
Placing an Order#
Both families produce a buildPlaceOrderIx compatible instruction; the
difference is in how accounts are resolved.
Trust-based#
The @seesaw/core API client resolves token accounts and config for you.
For wallet-facing order flow, prefer the high-level SDK helpers or the
trustless resolver below. The build*Ix functions are raw ABI encoders:
they preserve caller-supplied values and account metas, and they are best
reserved for wrappers, tests, and operator tooling that already performed
validation.
Supply the market address (from the API response) and your ATAs:
// docs-check: fragment reason: shared transaction setup
import { address } from '@solana/addresses';
import { createSolanaRpc } from '@solana/rpc';
import {
createSeesawClient,
buildPlaceOrderIx,
OrderSide,
OrderType,
pickTreasuryIndex,
parseSeesawError,
} from '@seesaw/core';
import { TOKEN_PROGRAM_ADDRESS, SYSTEM_PROGRAM_ADDRESS } from '@seesaw/trustless';
// ── Trust-based: resolve fee config via hosted API ─────────────────────────
const client = createSeesawClient({ apiUrl: process.env.SEESAW_API_URL! });
const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL!);
const marketAddress = address('MARKET_ADDRESS');
const user = address('YOUR_WALLET');
// Pick a treasury recipient index explicitly and pass the matching address.
const feeConfig = await client.api.fees.getConfig();
const slot = BigInt(await rpc.getSlot().send());
const protocolTreasuryIndex = pickTreasuryIndex(user, slot, feeConfig.treasuryRecipients.length);
const treasuryTokenAccount = address(feeConfig.treasuryRecipients[protocolTreasuryIndex]);
try {
const ix = buildPlaceOrderIx(
{
side: OrderSide.BuyYes,
priceBps: 6_000, // 60%
quantity: 10_000_000n, // 10 USDT (6 decimals)
orderType: OrderType.Limit,
protocolTreasuryIndex,
},
{
market: marketAddress,
orderbook: address('ORDERBOOK_PDA'), // deriveOrderbookPda(market)
userPosition: address('POSITION_PDA'), // derivePositionPda(market, user)
userTokenAccount: address('USER_SETTLEMENT_ATA'),
vault: address('VAULT_PDA'), // deriveVaultPda(market)
user,
config: address('CONFIG_PDA'), // deriveConfigPda()
treasuryTokenAccount,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
settlementMint: address('SETTLEMENT_MINT'),
yesEscrow: address('YES_ESCROW_PDA'),
noEscrow: address('NO_ESCROW_PDA'),
userYesAta: address('USER_YES_ATA'),
userNoAta: address('USER_NO_ATA'),
yesMint: address('YES_MINT_PDA'),
noMint: address('NO_MINT_PDA'),
traderLedger: address('TRADER_LEDGER_PDA'),
}
);
await sendInstructions([ix], keyPair, process.env.SOLANA_RPC_URL!);
} catch (error) {
const parsed = parseSeesawError(error);
if (parsed) console.error(`${parsed.name}: ${parsed.message}`);
else throw error;
}
The high-level placeOrder and free-funds helpers can instead accept the full
treasuryRecipients array and derive the writable treasury account from
protocolTreasuryIndex. If both treasuryRecipients and
treasuryTokenAccount are supplied, those helpers reject mismatches before
building the instruction. Raw build*Ix encoders preserve the caller-supplied
account list, so wrappers using them must keep the account and index paired.
In practice you derive all PDAs up front with the PDA helpers:
// docs-check: fragment reason: shared transaction setup
import {
deriveConfigPda,
deriveMarketPda,
deriveOrderbookPda,
deriveVaultPda,
derivePositionPda,
deriveYesMintPda,
deriveNoMintPda,
deriveYesEscrowPda,
deriveNoEscrowPda,
deriveTraderLedgerPda,
} from '@seesaw/core';
import { deriveAta } from '@seesaw/trustless';
const [configPda] = await deriveConfigPda();
const [orderbook] = await deriveOrderbookPda(marketAddress);
const [vault] = await deriveVaultPda(marketAddress);
const [userPosition] = await derivePositionPda(marketAddress, user);
const [yesMint] = await deriveYesMintPda(marketAddress);
const [noMint] = await deriveNoMintPda(marketAddress);
const [yesEscrow] = await deriveYesEscrowPda(marketAddress);
const [noEscrow] = await deriveNoEscrowPda(marketAddress);
const [traderLedger] = await deriveTraderLedgerPda(marketAddress);
const userSettlementAta = await deriveAta(user, settlementMint);
const userYesAta = await deriveAta(user, yesMint);
const userNoAta = await deriveAta(user, noMint);
Trustless#
resolvePlaceOrder derives and validates every account from chain state, picks
the protocol-fee recipient, and returns idempotent ATA-create instructions for
any missing token accounts. Prepend them to the same transaction:
// docs-check: fragment reason: shared transaction setup
import { TrustlessRpc, TrustlessResolver, listMarkets } from '@seesaw/trustless';
import { buildPlaceOrderIx, OrderSide, OrderType, parseSeesawError } from '@seesaw/core';
import { TOKEN_PROGRAM_ADDRESS, SYSTEM_PROGRAM_ADDRESS } 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('YOUR_WALLET');
// Discover live markets from chain state (no Seesaw API needed).
const { value: markets } = await listMarkets(rpc);
const marketAddress = markets[0].address;
// Resolve every account PlaceOrder needs, including precondition checks
// (pause, emergency status, trading window, referral).
const r = await resolver.resolvePlaceOrder({ marketAddress, user });
// r.accounts — all PDAs and token accounts, validated
// r.protocolTreasuryIndex — deterministically picked, fee recipient verified
// r.referral — on-chain referral status (state: 'none' | 'expired' | 'active')
// r.prependInstructions — idempotent ATA-creates for any missing token accounts
// r.slot — slot the reads were anchored at
const ix = buildPlaceOrderIx(
{
side: OrderSide.BuyNo,
priceBps: 4_000, // 40% NO = 60% implied YES
quantity: 10_000_000n,
orderType: OrderType.Limit,
protocolTreasuryIndex: r.protocolTreasuryIndex,
},
{
...r.accounts,
user,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
// Attach the 3-account referral tail when the user has an active referral.
...(r.referral.state === 'active' && r.referral.accounts
? {
takerReferralAccount: r.referral.accounts.referralAccount,
referrerEarningsAccount: r.referral.accounts.referrerEarningsAccount,
referrerTreasury: r.referral.accounts.referrerTreasury,
}
: {}),
}
);
// Prepend any ATA-create instructions the resolver generated.
await sendInstructions([...r.prependInstructions, ix], keyPair, process.env.SOLANA_RPC_URL!);
About the referral tail. Seesaw splits the taker fee three ways: 50%
protocol, 10% market creator, 40% referrer. When r.referral.state === 'active'
you append three extra accounts (takerReferralAccount,
referrerEarningsAccount, referrerTreasury) to PlaceOrder. If the referral
is absent or expired, the 40% referral slice routes to the protocol — the trade
succeeds either way. See the
Referral and Creator Fees guide for the full
precedence rules.
Cancelling an Order#
Trust-based#
// docs-check: fragment reason: shared transaction setup
import {
buildCancelOrderIx,
deriveOrderbookPda,
deriveVaultPda,
derivePositionPda,
deriveYesMintPda,
deriveNoMintPda,
deriveYesEscrowPda,
deriveNoEscrowPda,
deriveTraderLedgerPda,
} from '@seesaw/core';
import { TOKEN_PROGRAM_ADDRESS, deriveAta } from '@seesaw/trustless';
// orderId comes from the API (client.api.orders.list) or from
// getOrdersByOwner(orderbook, user) after decoding the on-chain book.
const orderId = 42n;
const [orderbook] = await deriveOrderbookPda(marketAddress);
const [vault] = await deriveVaultPda(marketAddress);
const [userPosition] = await derivePositionPda(marketAddress, user);
const [yesMint] = await deriveYesMintPda(marketAddress);
const [noMint] = await deriveNoMintPda(marketAddress);
const [yesEscrow] = await deriveYesEscrowPda(marketAddress);
const [noEscrow] = await deriveNoEscrowPda(marketAddress);
const [traderLedger] = await deriveTraderLedgerPda(marketAddress);
const userTokenAccount = await deriveAta(user, settlementMint);
const userYesAta = await deriveAta(user, yesMint);
const userNoAta = await deriveAta(user, noMint);
const ix = buildCancelOrderIx(
{ orderId },
{
market: marketAddress,
orderbook,
userPosition,
userTokenAccount,
vault,
user,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
settlementMint,
yesEscrow,
noEscrow,
userYesAta,
userNoAta,
yesMint,
noMint,
traderLedger,
}
);
await sendInstructions([ix], keyPair, process.env.SOLANA_RPC_URL!);
Trustless#
resolveTradingAccounts resolves the shared account set used by cancel, reduce,
mint, redeem, and withdraw. requireUserOrder confirms the order id exists on
the on-chain book before you build the instruction:
// docs-check: fragment reason: shared transaction setup
import { TrustlessRpc, TrustlessResolver, TOKEN_PROGRAM_ADDRESS } from '@seesaw/trustless';
import { buildCancelOrderIx } from '@seesaw/core';
const rpc = new TrustlessRpc({ url: process.env.SOLANA_RPC_URL! });
const resolver = new TrustlessResolver(rpc);
const orderId = 42n;
// Fail-closed order-id verification: throws NotFoundOnChainError if the
// order is not on the book or does not belong to `user`.
await resolver.requireUserOrder(marketAddress, user, orderId);
const { value: accts, slot } = await resolver.resolveTradingAccounts({ marketAddress, user });
const ix = buildCancelOrderIx(
{ orderId },
{
...accts,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
}
);
// Prepend any missing-ATA instructions (e.g., first time holding YES shares).
await sendInstructions([...accts.prependInstructions, ix], keyPair, process.env.SOLANA_RPC_URL!);
Redeeming After Resolution#
Redeem burns outcome shares and pays out stablecoin. It also releases any
remaining resting-bid collateral that was locked when you placed buy orders, so
always call it even if your position has zero winning shares. The market must
be in Resolved or Expired state.
Timeline warning. 7 days after
resolved_atthe market becomes eligible forCloseMarketteardown.MarkPositionSettled(a permissionless crank) pays out unclaimed positions to their owners first, but anything still unclaimed whenCloseMarketexecutes sweeps to the market creator. Redeem promptly.
Trust-based#
// docs-check: fragment reason: shared transaction setup
import {
buildRedeemIx,
TokenType,
deriveYesMintPda,
deriveNoMintPda,
deriveVaultPda,
derivePositionPda,
deriveOrderbookPda,
deriveTraderLedgerPda,
} from '@seesaw/core';
import { TOKEN_PROGRAM_ADDRESS, deriveAta } from '@seesaw/trustless';
// After resolution, payout rates per outcome:
// UP: YES = 10000/10000, NO = 0/10000
// DOWN: YES = 0/10000, NO = 10000/10000
// EXPIRED: YES = 5000/10000, NO = 5000/10000
const [yesMint] = await deriveYesMintPda(marketAddress);
const [noMint] = await deriveNoMintPda(marketAddress);
const [vault] = await deriveVaultPda(marketAddress);
const [userPosition] = await derivePositionPda(marketAddress, user);
const [orderbook] = await deriveOrderbookPda(marketAddress);
const [traderLedger] = await deriveTraderLedgerPda(marketAddress);
const userStablecoinAta = await deriveAta(user, settlementMint);
const userYesAta = await deriveAta(user, yesMint);
const userNoAta = await deriveAta(user, noMint);
// Redeem YES shares (call once for each token type you hold).
const redeemYesIx = buildRedeemIx(
{ amount: yesShareBalance, tokenType: TokenType.Yes },
{
market: marketAddress,
yesMint,
noMint,
userYesAta,
userNoAta,
userStablecoinAta,
vault,
user,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
settlementMint,
// Optional position/orderbook/traderLedger: pass them to release
// locked bid collateral in the same instruction.
userPosition,
orderbook,
traderLedger,
}
);
await sendInstructions([redeemYesIx], keyPair, process.env.SOLANA_RPC_URL!);
Trustless#
// docs-check: fragment reason: shared transaction setup
import { TrustlessRpc, TrustlessResolver, TOKEN_PROGRAM_ADDRESS } from '@seesaw/trustless';
import { buildRedeemIx, TokenType } from '@seesaw/core';
const rpc = new TrustlessRpc({ url: process.env.SOLANA_RPC_URL! });
const resolver = new TrustlessResolver(rpc);
// Verify the market is resolved and read the outcome.
const { value: market, slot } = await resolver.getMarket(marketAddress);
// market.outcome: 0=None, 1=Up, 2=Down, 3=Expired
const { value: position } = await resolver.getPosition(marketAddress, user);
if (!position || position.settled) {
console.log('Nothing to redeem.');
} else {
const { value: accts } = await resolver.resolveTradingAccounts({ marketAddress, user });
const redeemYesIx = buildRedeemIx(
{ amount: position.yesShares, tokenType: TokenType.Yes },
{
market: marketAddress,
yesMint: accts.yesMint,
noMint: accts.noMint,
userYesAta: accts.userYesAta,
userNoAta: accts.userNoAta,
userStablecoinAta: accts.userTokenAccount,
vault: accts.vault,
user,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
settlementMint: accts.settlementMint,
userPosition: accts.userPosition,
orderbook: accts.orderbook,
traderLedger: accts.traderLedger,
}
);
await sendInstructions(
[...accts.prependInstructions, redeemYesIx],
keyPair,
process.env.SOLANA_RPC_URL!
);
}
Creating a Market#
buildCreateMarketBundle returns a bundle of instructions:
N × EnsureTraderLedgerSpace (0x2E), M × EnsureDeepOrderbookSpace
(0x33), then one CreateMarket (0x03). Submit the instructions in order.
For larger seat counts or orderbook tiers, split the allocation preludes across
multiple transactions and submit the final CreateMarket instruction only
after the trader-ledger and orderbook PDAs have reached their target sizes. The
high-level createMarket beta wrapper is intentionally narrower: it derives
accounts for you but rejects bundles that exceed the current one-shot limits
instead of planning split transactions.
Why the prelude is required#
The trader ledger and deep orderbook PDAs can be several hundred kilobytes
(depending on seat count and orderbook tier). Solana caps per-instruction
account growth at 10,240 bytes (MAX_PERMITTED_DATA_INCREASE).
EnsureTraderLedgerSpace and EnsureDeepOrderbookSpace grow their accounts by
up to 10,240 bytes per call, and the SDK computes the exact number of calls
needed. The instructions are idempotent — over-invoking is safe.
For the default standard trader-ledger tier (1,025 seats ≈ 74 KB), the bundle
contains roughly 8 ledger prelude instructions plus the orderbook preludes and
CreateMarket. For the largest seat or orderbook tiers, the allocation
preludes may need to be split across multiple transactions.
// docs-check: fragment reason: shared transaction setup
import {
buildCreateMarketBundle,
deriveMarketPda,
deriveOrderbookPda,
deriveVaultPda,
deriveYesMintPda,
deriveNoMintPda,
deriveYesEscrowPda,
deriveNoEscrowPda,
deriveTraderLedgerPda,
deriveAssetPda,
deriveConfigPda,
CAPACITY_TIERS,
PROGRAM_ADDRESS,
resolveMarketIdFromClock,
} from '@seesaw/core';
import { SYSTEM_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS } from '@seesaw/trustless';
import { address, type Address } from '@solana/addresses';
const feedId = new Uint8Array(32); // 32-byte Pyth price feed id
const durationSeconds = 900n; // 15-minute markets
const creator = address('YOUR_WALLET');
const pythFeed = address('PYTH_FEED_ACCOUNT'); // Push-mode: actual feed address
const settlementMint = address('USDT_MINT');
// CreateMarket PDAs must use Solana Clock, not local/server wall-clock time.
const marketId = await resolveMarketIdFromClock(fetchAccountData, durationSeconds);
async function fetchAccountData(account: Address): Promise<Uint8Array | null> {
// Read `account` from your Solana RPC with base64 encoding and return bytes.
// The resolver passes Clock sysvar here.
throw new Error('wire this to your RPC client');
}
// Derive all PDAs
const [marketPda] = await deriveMarketPda(feedId, durationSeconds, marketId, creator);
const [orderbookPda] = await deriveOrderbookPda(marketPda);
const [vaultPda] = await deriveVaultPda(marketPda);
const [yesMint] = await deriveYesMintPda(marketPda);
const [noMint] = await deriveNoMintPda(marketPda);
const [yesEscrow] = await deriveYesEscrowPda(marketPda);
const [noEscrow] = await deriveNoEscrowPda(marketPda);
const [traderLedger] = await deriveTraderLedgerPda(marketPda);
const [assetState] = await deriveAssetPda(feedId);
const [configPda] = await deriveConfigPda();
// Pick a trader-ledger tier: 'small' (128), 'standard' (1025), or 'large' (4097).
// Raw seat counts are available separately as TRADER_LEDGER_SEAT_OPTIONS.
const tier = CAPACITY_TIERS.find((t) => t.id === 'standard')!;
const instructions = buildCreateMarketBundle(
{
maxConfidenceRatioBps: 500, // 5% confidence/price ratio tolerance
durationSeconds,
maxOracleJumpBps: 2_000, // 20% maximum inter-observation jump
marketSizeParams: {
bidsSize: 512, // deep-orderbook tier: 64, 128, 256, 512, 1024, 2048, or 4096
asksSize: 512, // must match bidsSize
numSeats: tier.numSeats,
},
oracleMode: 'pull', // launch default; use 'push' only for legacy-compatible feed accounts
},
{
market: marketPda,
orderbook: orderbookPda,
vault: vaultPda,
yesMint,
noMint,
assetState,
config: configPda,
pythFeed,
settlementMint,
payer: creator,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
yesEscrow,
noEscrow,
traderLedger,
}
);
// instructions[0..N-1] = EnsureTraderLedgerSpace (0x2E) prelude
// instructions[N..M-1] = EnsureDeepOrderbookSpace (0x33) prelude
// instructions[M] = CreateMarket (0x03)
// For small and some mid-size tiers, the whole bundle may fit in one transaction.
// For large/max tiers, split the prelude across transactions:
const MAX_PRELUDE_PER_TX = 10; // rough upper bound for a single tx at 200k CU
const prelude = instructions.slice(0, -1);
const createMarketIx = instructions[instructions.length - 1]!;
for (let i = 0; i < prelude.length; i += MAX_PRELUDE_PER_TX) {
await sendInstructions(
prelude.slice(i, i + MAX_PRELUDE_PER_TX),
keyPair,
process.env.SOLANA_RPC_URL!
);
}
await sendInstructions([createMarketIx], keyPair, process.env.SOLANA_RPC_URL!);
The high-level createMarket function from @seesaw/core (see
packages/core/README.md) derives PDAs for you; use it to reduce boilerplate
for single-transaction beta market creation. For 8,193/8,321-seat ledgers, use
the lower-level builder above and explicitly split the prelude before sending
the final CreateMarket.
Free-Funds Order Variants#
The *WithFreeFunds variants (buildPlaceLimitOrderWithFreeFundsIx,
buildSwapWithFreeFundsIx, buildPlaceMultiplePostOnlyOrdersWithFreeFundsIx)
skip the SPL token-transfer step and source quote/share inventory from the
caller's trader-ledger free buckets. Placement variants still use the full
place-order account layout because matching, fee routing, escrow, mint, and
referral validation all remain live. The lightweight five-account layout is
only for cancel/reduce free-funds variants.
Share-side asks (SellYes / BuyNo with free funds) are fully supported.
When a free-funds ask rests on the book, the on-chain program mints the resting
quantity into the YES/NO escrow PDA so the book entry is backed by real SPL
inventory. On cancel, released shares return to the maker's SPL ATA (not back
into the ledger free buckets).
// docs-check: fragment reason: shared transaction setup
import { address } from '@solana/addresses';
import {
buildPlaceLimitOrderWithFreeFundsIx,
OrderSide,
OrderType,
deriveOrderbookPda,
derivePositionPda,
deriveTraderLedgerPda,
deriveVaultPda,
deriveConfigPda,
deriveYesEscrowPda,
deriveNoEscrowPda,
deriveYesMintPda,
deriveNoMintPda,
} from '@seesaw/core';
// To cancel a free-funds order, use buildCancelOrderIx (same as a standard cancel)
// or the batch helpers: buildCancelAllOrdersWithFreeFundsIx,
// buildCancelUpToWithFreeFundsIx, buildCancelMultipleOrdersByIdWithFreeFundsIx.
const [orderbook] = await deriveOrderbookPda(marketAddress);
const [userPosition] = await derivePositionPda(marketAddress, user);
const [vault] = await deriveVaultPda(marketAddress);
const [config] = await deriveConfigPda();
const [yesEscrow] = await deriveYesEscrowPda(marketAddress);
const [noEscrow] = await deriveNoEscrowPda(marketAddress);
const [yesMint] = await deriveYesMintPda(marketAddress);
const [noMint] = await deriveNoMintPda(marketAddress);
const [traderLedger] = await deriveTraderLedgerPda(marketAddress);
const tokenProgram = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const systemProgram = address('11111111111111111111111111111111');
// Caller-owned token accounts and configured fee/mint accounts.
const userTokenAccount = userStablecoinAta;
// PlaceLimitOrderWithFreeFunds — no token-transfer CPI, full place-order metas.
const ffIx = buildPlaceLimitOrderWithFreeFundsIx(
{
side: OrderSide.BuyYes,
priceBps: 5_500,
quantity: 5_000_000n,
orderType: OrderType.PostOnly,
protocolTreasuryIndex,
},
{
market: marketAddress,
orderbook,
userPosition,
userTokenAccount,
vault,
user,
config,
treasuryTokenAccount,
tokenProgram,
systemProgram,
settlementMint,
yesEscrow,
noEscrow,
userYesAta,
userNoAta,
yesMint,
noMint,
traderLedger,
}
);
await sendInstructions([ffIx], keyPair, process.env.SOLANA_RPC_URL!);
Priority Fees and Compute Budget#
Add a SetComputeUnitPrice instruction before your Seesaw instruction for
priority scheduling. The compute-budget program address is a well-known constant:
// docs-check: fragment reason: shared transaction setup
import { address } from '@solana/addresses';
import type { Instruction } from '@solana/instructions';
import { AccountRole } from '@solana/instructions';
import { getU32Encoder, getU64Encoder } from '@solana/codecs';
const COMPUTE_BUDGET_PROGRAM = address('ComputeBudget111111111111111111111111111111');
function setComputeUnitPrice(microLamports: bigint): Instruction {
const data = new Uint8Array(9);
data[0] = 3; // SetComputeUnitPrice discriminator
const encoded = new Uint8Array(getU64Encoder().encode(microLamports));
data.set(encoded, 1);
return { programAddress: COMPUTE_BUDGET_PROGRAM, accounts: [], data };
}
function setComputeUnitLimit(units: number): Instruction {
const data = new Uint8Array(5);
data[0] = 2; // SetComputeUnitLimit discriminator
const encoded = new Uint8Array(getU32Encoder().encode(units));
data.set(encoded, 1);
return { programAddress: COMPUTE_BUDGET_PROGRAM, accounts: [], data };
}
// Prepend budget instructions to your instruction list:
const instructions = [
setComputeUnitLimit(200_000),
setComputeUnitPrice(10_000n), // microLamports; tune to current network conditions
ix, // your Seesaw instruction
];
await sendInstructions(instructions, keyPair, process.env.SOLANA_RPC_URL!);
Simulate Before Signing with the Trustless RPC#
TrustlessRpc.simulateTransaction accepts a base64-encoded wire transaction and
runs it against the current cluster state with replaceRecentBlockhash: true
and sigVerify: false, so you can simulate before the user signs. This is
safeguard #8 from the trustless trust model.
// docs-check: fragment reason: shared transaction setup
import { TrustlessRpc, SimulationFailedError } from '@seesaw/trustless';
import {
compileTransaction,
getBase64EncodedWireTransaction,
// ... rest of your Kit transaction assembly imports
} from '@solana/transaction-messages';
const rpc = new TrustlessRpc({ url: process.env.SOLANA_RPC_URL! });
// Assemble + compile (but don't sign yet)
const compiled = compileTransaction(msg);
const base64Tx = getBase64EncodedWireTransaction(compiled);
try {
const sim = await rpc.simulateTransaction(base64Tx);
if (sim.err) {
throw new SimulationFailedError(sim.logs);
}
console.log(`Simulation passed — ${sim.unitsConsumed ?? 'unknown'} CU consumed`);
// Now sign and send with your wallet stack.
} catch (e) {
if (e instanceof SimulationFailedError) {
console.error('Simulation failed:\n', e.logs.join('\n'));
}
throw e;
}
Error Handling#
@seesaw/core exports parseSeesawError which extracts a structured error from
an on-chain transaction rejection. The trustless family throws typed errors that
wrap RPC and validation failures:
// docs-check: fragment reason: shared transaction setup
import { parseSeesawError, SeesawErrorCode } from '@seesaw/core';
import {
AccountNotFoundError,
AccountValidationError,
PreconditionError,
SimulationFailedError,
} from '@seesaw/trustless';
try {
await sendInstructions(instructions, keyPair, rpcUrl);
} catch (error) {
// On-chain program errors (from transaction logs)
const parsed = parseSeesawError(error);
if (parsed) {
console.error(`Program error ${parsed.name}: ${parsed.message}`);
return;
}
// Trustless resolution errors (before the transaction is built)
if (error instanceof PreconditionError) {
// e.g., market paused, trading window closed, market already resolved
console.error(`Precondition: ${error.kind} — ${error.message}`);
} else if (error instanceof AccountValidationError) {
console.error(`Account invalid: ${error.account} — ${error.message}`);
} else if (error instanceof AccountNotFoundError) {
console.error(`Account missing: ${error.account}`);
} else if (error instanceof SimulationFailedError) {
console.error('Simulation failed:\n', error.logs.join('\n'));
} else {
throw error;
}
}
Crank Instructions#
These permissionless instructions advance the market lifecycle. Any wallet can
call them and earn closer_reward_lamports (default 200,000 lamports,
admin-tunable) for snapshot/resolve/expire.
Snapshot End#
Captures the end price from the Pyth feed after t_end. Sampling Rule A: first
price with publish_time >= t_end:
// docs-check: fragment reason: shared transaction setup
import { buildSnapshotEndIx, deriveConfigPda } from '@seesaw/core';
const [configPda] = await deriveConfigPda();
// market.pythFeed: the feed address for push-mode markets.
// For pull-mode markets supply the ephemeral PriceUpdateV2 account instead.
const ix = buildSnapshotEndIx({
market: marketAddress,
pythFeed: address('PYTH_FEED_ACCOUNT'),
caller: crankerAddress,
config: configPda,
});
await sendInstructions([ix], crankerKeyPair, process.env.SOLANA_RPC_URL!);
Resolve Market#
Computes UP (P_end >= P_start) or DOWN and transitions the market to
Resolved. Must be called after a successful SnapshotEnd:
// docs-check: fragment reason: shared transaction setup
import {
buildResolveMarketIx,
deriveConfigPda,
deriveVaultPda,
deriveAssetPda,
} from '@seesaw/core';
const [configPda] = await deriveConfigPda();
const [vaultPda] = await deriveVaultPda(marketAddress);
const [assetState] = await deriveAssetPda(feedId);
const ix = buildResolveMarketIx({
market: marketAddress,
assetState,
caller: crankerAddress,
config: configPda,
vault: vaultPda,
creatorTokenAccount: address('CREATOR_SETTLEMENT_ATA'),
settlementMint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
await sendInstructions([ix], crankerKeyPair, process.env.SOLANA_RPC_URL!);
Next Steps#
- See copy-pasteable end-to-end programs in Examples
- Full account-resolution trust model in the Trustless SDK guide
- Fee math and referral setup in the Referral and Creator Fees guide
- Where funds live at every step: Flow of Funds