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.
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: 'confirmed' }).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: 'confirmed',
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: 'confirmed',
})
.send();
// 5. Confirm
await rpc
.confirmTransaction(sig, {
commitment: 'confirmed',
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.
Supply the market address (from the API response) and your ATAs:
import { address } from '@solana/addresses';
import {
createSeesawClient,
buildPlaceOrderIx,
OrderSide,
OrderType,
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 marketAddress = address('MARKET_ADDRESS');
const user = address('YOUR_WALLET');
// Fetch the treasury recipient index + address from the API (or pick index 0).
const feeConfig = await client.api.fees.getConfig();
const treasuryTokenAccount = address(feeConfig.treasuryRecipients[0]);
const protocolTreasuryIndex = 0;
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;
}
In practice you derive all PDAs up front with the PDA helpers:
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:
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 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#
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:
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#
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#
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 (and the high-level createMarket wrapper) return a
bundle of instructions: N × EnsureTraderLedgerSpace (0x2E) followed by
one CreateMarket (0x03). Send all instructions in order in a single
transaction.
Why the prelude is required#
The trader ledger PDA can be several hundred kilobytes (depending on seat
count). Solana caps per-instruction account growth at 10,240 bytes
(MAX_PERMITTED_DATA_INCREASE). EnsureTraderLedgerSpace grows the account by
up to 10,240 bytes per call, and the SDK computes the exact number of calls
needed: ceil(traderLedgerAccountSize(numSeats) / 10240). The instruction is
idempotent — over-invoking is safe.
For the default standard capacity tier (1,025 seats ≈ 86 KB), the bundle
contains roughly 9 prelude instructions plus the CreateMarket. For the
largest tier (8,321 seats ≈ 680 KB), the prelude has ~67 instructions and may
need to be split across multiple transactions.
import {
buildCreateMarketBundle,
deriveMarketPda,
deriveOrderbookPda,
deriveVaultPda,
deriveYesMintPda,
deriveNoMintPda,
deriveYesEscrowPda,
deriveNoEscrowPda,
deriveTraderLedgerPda,
deriveAssetPda,
deriveConfigPda,
CAPACITY_TIERS,
PROGRAM_ADDRESS,
getCurrentMarketId,
} from '@seesaw/core';
import { SYSTEM_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS } from '@seesaw/trustless';
import { 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');
const marketId = BigInt(getCurrentMarketId(Number(durationSeconds)));
// 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 capacity tier: 'small' (128), 'standard' (1025), 'large' (4097), 'max' (8321)
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,
asksSize: 512,
numSeats: tier.numSeats,
},
oracleMode: 'push', // 'push' (default) or 'pull'
},
{
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] = CreateMarket (0x03)
// For standard/small tiers, the whole bundle fits 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
in scripts that already have marketId, pythFeedId, and creator at hand.
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. They require fewer accounts and are faster
for makers running tight inventory loops.
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).
import {
buildPlaceLimitOrderWithFreeFundsIx,
OrderSide,
OrderType,
deriveOrderbookPda,
derivePositionPda,
deriveTraderLedgerPda,
} 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 [traderLedger] = await deriveTraderLedgerPda(marketAddress);
// PlaceLimitOrderWithFreeFunds — no token-transfer CPI, 5 accounts vs 18+
const ffIx = buildPlaceLimitOrderWithFreeFundsIx(
{
side: OrderSide.BuyYes,
priceBps: 5_500,
quantity: 5_000_000n,
orderType: OrderType.PostOnly,
protocolTreasuryIndex: 0,
},
{
market: marketAddress,
orderbook,
userPosition,
user,
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:
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.
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:
import { parseSeesawError, SeesawErrorCode } from '@seesaw/core';
import {
AccountNotFoundError,
AccountValidationError,
PreconditionError,
SimulationFailedError,
} from '@seesaw/trustless';
try {
await sendInstructions([...], 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:
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:
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