Runnable Node trading example#
Use this complete example with the quickstart. It reads a
native market, sends a bounded Buy YES IOC, prints the resulting position,
and redeems an explicit amount after settlement. Save it as quickstart.ts
where the checkout's dependencies resolve. Run read, trade, or redeem;
the default is read-only.
ts
// docs-check: semantic
import { appendFileSync, readFileSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import {
address,
getAddressFromPublicKey,
getAddressEncoder,
type Address,
} from '@solana/addresses';
import type { Instruction } from '@solana/instructions';
import { createKeyPairFromBytes } from '@solana/keys';
import { createSolanaRpc } from '@solana/rpc';
import {
appendTransactionMessageInstructions,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
} from '@solana/transaction-messages';
import {
compileTransaction,
getBase64EncodedWireTransaction,
getSignatureFromTransaction,
signTransaction,
} from '@solana/transactions';
import {
createSeesawClient,
computeBudgetPrefixFor,
decodeConfigAccount,
decodeDeepOrderbookAccount,
decodeMarketAccount,
decodePositionAccount,
deriveConfigPda,
deriveMarketPda,
deriveNoMintPda,
deriveOrderbookPda,
derivePositionPda,
deriveReferralPda,
deriveYesMintPda,
InstructionDiscriminator,
OrderSide,
OrderType,
} from '@seesaw/core';
const required = (name: string): string => {
const value = process.env[name];
if (!value) throw new Error(`Set ${name}`);
return value;
};
const print = (value: unknown) =>
console.log(
JSON.stringify(value, (_, item) => (typeof item === 'bigint' ? item.toString() : item), 2)
);
const mode = process.argv[2] ?? 'read';
if (!['read', 'trade', 'redeem'].includes(mode)) throw new Error('Use read, trade, or redeem');
const program = address(required('SEESAW_PROGRAM_ADDRESS'));
const settlementMint = address(required('SEESAW_SETTLEMENT_MINT'));
const creator = address(required('SEESAW_CREATOR'));
const rpc = createSolanaRpc(required('SEESAW_RPC_URL'));
const keypairPath = required('SEESAW_KEYPAIR');
if ((statSync(keypairPath).mode & 0o077) !== 0)
throw new Error('Use a private keypair file (chmod 600)');
const keyBytes: unknown = JSON.parse(readFileSync(keypairPath, 'utf8'));
if (
!Array.isArray(keyBytes) ||
keyBytes.length !== 64 ||
keyBytes.some((v) => !Number.isInteger(v) || v < 0 || v > 255)
)
throw new Error('Expected a 64-byte Solana keypair');
const keyPair = await createKeyPairFromBytes(Uint8Array.from(keyBytes));
const user = await getAddressFromPublicKey(keyPair.publicKey);
const duration = BigInt(required('SEESAW_DURATION_SECONDS'));
if (duration <= 0n) throw new Error('Duration must be positive');
const feedHex = required('SEESAW_PYTH_FEED_ID_HEX').replace(/^0x/, '');
if (!/^[0-9a-f]{64}$/i.test(feedHex)) throw new Error('Feed ID must contain 32 bytes');
const feed = Uint8Array.from(Buffer.from(feedHex, 'hex'));
if (mode !== 'read') required('SEESAW_MARKET_ID');
const marketId = BigInt(
process.env.SEESAW_MARKET_ID ?? (BigInt(Math.floor(Date.now() / 1000)) / duration).toString()
);
const [market, marketBump] = await deriveMarketPda(feed, duration, marketId, creator, program);
const [config] = await deriveConfigPda(program);
const [orderbook] = await deriveOrderbookPda(market, program);
const [position] = await derivePositionPda(market, user, program);
const [yesMint] = await deriveYesMintPda(market, program);
const [noMint] = await deriveNoMintPda(market, program);
async function accountBytes(account: Address, owner: Address, optional = false) {
const response = await rpc
.getAccountInfo(account, { encoding: 'base64', commitment: 'finalized' })
.send();
if (!response.value) {
if (optional) return null;
throw new Error(`Missing account ${account}`);
}
if (response.value.owner !== owner) throw new Error(`Unexpected owner for ${account}`);
return Uint8Array.from(Buffer.from(response.value.data[0], 'base64'));
}
async function readState() {
const state = decodeMarketAccount((await accountBytes(market, program))!);
if (
state.marketKind !== 0 ||
state.marketId !== marketId ||
state.creator !== creator ||
state.durationSeconds !== duration ||
state.bump !== marketBump ||
!Buffer.from(state.pythFeedId).equals(Buffer.from(feed)) ||
state.settlementMint !== settlementMint
) {
throw new Error('Market identity does not match the requested native series');
}
const positionBytes = await accountBytes(position, program, true);
print({
user,
market,
marketId,
settlementMint,
yesMint,
noMint,
outcome: state.outcome,
book: decodeDeepOrderbookAccount((await accountBytes(orderbook, program))!),
position: positionBytes ? decodePositionAccount(positionBytes) : null,
});
return state;
}
async function send(instructions: readonly Instruction[]) {
const { value: lifetime } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send();
const message = appendTransactionMessageInstructions(
instructions,
setTransactionMessageLifetimeUsingBlockhash(
lifetime,
setTransactionMessageFeePayer(user, createTransactionMessage({ version: 0 }))
)
);
const transaction = compileTransaction(message);
const preview = await rpc
.simulateTransaction(getBase64EncodedWireTransaction(transaction), {
encoding: 'base64',
sigVerify: false,
commitment: 'confirmed',
})
.send();
if (preview.value.err) throw new Error(`Simulation failed: ${JSON.stringify(preview.value.err)}`);
const signed = await signTransaction([keyPair], transaction);
const signature = getSignatureFromTransaction(signed);
const wire = getBase64EncodedWireTransaction(signed);
if (Buffer.from(wire, 'base64').length > 1232)
throw new Error('Transaction exceeds the v0 packet limit');
const journal =
process.env.SEESAW_JOURNAL ?? join(tmpdir(), 'seesaw-quickstart-signatures.jsonl');
appendFileSync(
journal,
JSON.stringify({
market,
signature,
blockhash: lifetime.blockhash,
lastValidBlockHeight: lifetime.lastValidBlockHeight.toString(),
}) + '\n',
{ mode: 0o600 }
);
print({ signature, journal });
// Preserve this signature if submission times out; do not blindly rebuild.
await rpc
.sendTransaction(wire, { encoding: 'base64', skipPreflight: false, maxRetries: 0n })
.send();
for (let attempt = 0; attempt < 60; attempt++) {
const { value: statuses } = await rpc
.getSignatureStatuses([signature], { searchTransactionHistory: true })
.send();
const status = statuses[0];
if (status?.err) throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
if (status?.confirmationStatus === 'finalized') return signature;
await delay(1000);
}
throw new Error(`Confirmation remains unresolved. Reconcile ${signature} before retrying.`);
}
const state = await readState();
if (mode !== 'read') {
const tokenProgram = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const userTokenAccount = address(required('SEESAW_USER_SETTLEMENT_ACCOUNT'));
const userYesAta = address(required('SEESAW_USER_YES_ACCOUNT'));
const userNoAta = address(required('SEESAW_USER_NO_ACCOUNT'));
for (const [account, mint] of [
[userTokenAccount, settlementMint],
[userYesAta, yesMint],
[userNoAta, noMint],
]) {
const bytes = (await accountBytes(account!, tokenProgram))!;
if (
bytes.length !== 165 ||
!Buffer.from(bytes.subarray(0, 32)).equals(Buffer.from(getAddressEncoder().encode(mint!))) ||
!Buffer.from(bytes.subarray(32, 64)).equals(Buffer.from(getAddressEncoder().encode(user)))
) {
throw new Error(`Token account mint/owner mismatch: ${account}`);
}
}
const client = createSeesawClient();
if (mode === 'trade') {
const cfg = decodeConfigAccount((await accountBytes(config, program))!);
if (cfg.defaultSettlementMint !== settlementMint) throw new Error('Config mint mismatch');
const [referral] = await deriveReferralPda(user, program);
const referralBytes = await accountBytes(referral, program, true);
const quantity = BigInt(required('SEESAW_QUANTITY'));
const priceBps = Number(required('SEESAW_PRICE_BPS'));
const built = await client.ixs.placeOrder(
{
marketAddress: market,
marketState: state,
user,
userTokenAccount,
userYesAta,
userNoAta,
settlementMint,
treasuryRecipients: cfg.treasuryRecipients,
protocolTreasuryIndex: 0,
takerReferralAccount: referralBytes ? referral : undefined,
side: OrderSide.BuyYes,
orderType: OrderType.ImmediateOrCancel,
priceBps,
quantity,
worstAcceptablePriceBps: priceBps,
minFillQuantity: quantity,
selfTradeBehavior: 0,
},
program
);
await send([...computeBudgetPrefixFor(InstructionDiscriminator.PlaceOrder), built.instruction]);
} else {
if (state.outcome === 0) throw new Error('Market has not reached a terminal outcome');
const built = await client.ixs.redeem(
{
marketAddress: market,
user,
userYesAta,
userNoAta,
userStablecoinAta: userTokenAccount,
settlementMint,
orderbook,
amount: BigInt(required('SEESAW_REDEEM_AMOUNT')),
tokenType: 0,
},
program
);
await send([built.instruction]);
}
await readState();
}
Account reads are finalized but are not an atomic multi-account snapshot. Simulation and on-chain validation check the transaction's live inputs. The docs gate compiles this example; actual execution requires deployment, funds, liquidity, and later settlement. The temporary signature journal is a tutorial aid; an unattended bot needs the persistent recovery rules in Monitoring.