Automation#
Automated trading on Seesaw means reading on-chain state, applying strategy logic, and submitting signed instructions programmatically — without a UI.
This page covers two related topics:
- Permissionless lifecycle cranks — anyone can run the market lifecycle (create, snapshot, resolve, expire) and earn SOL rewards for doing so.
- Automated trading bots — placing and cancelling orders, managing inventory, and handling settlement automatically.
Permissionless cranks#
Seesaw has no required keeper or operator. Every lifecycle step is a public instruction that anyone can call. The protocol rewards timely execution with lamport payments:
Crank rewards#
Each successful lifecycle instruction earns a SOL reward paid directly from the market account:
| Operation | Trigger condition | Default reward |
|---|---|---|
CreateMarket | now >= t_start | 0.0002 SOL |
SnapshotEnd | now >= t_end AND Pyth has a valid post-boundary price | 0.0002 SOL |
ResolveMarket | End snapshot captured | 0.0002 SOL |
ExpireMarket | now >= t_end + expiration_window AND market unresolved | 0.0002 SOL |
The default reward is DEFAULT_CLOSER_REWARD_LAMPORTS = 200,000 lamports
(0.0002 SOL). Each market pre-funds up to 5 reward payouts at creation.
The reward is admin-tunable (via UpdateOperationalParams 0x2F) but only
for future markets — existing markets are funded at the rate in effect when
they were created.
Duplicate calls (e.g., two cranks racing to snapshot the same market) are safe — all lifecycle instructions are idempotent. The first successful call earns the reward; subsequent calls are no-ops and earn nothing.
ExpireMarket: three-tier fallback#
ExpireMarket is only available after t_end + market_expiration_window
(default 7 days from the spec — see MARKET_EXPIRY_WINDOW = 604800 s
in src/constants.rs). It tries to resolve normally before falling back:
| Tier | Condition | Result |
|---|---|---|
| 1 | Both start and end prices already captured | Normal UP/DOWN resolution |
| 2 | End price missing; valid Pyth price available | Late-capture end price, normal resolution |
| 3 | No oracle price available | 50/50 Expired split |
Tiers 1 and 2 mean users rarely get the 50/50 penalty — the expire crank first tries every reasonable path to a normal outcome.
Minimal crank loop#
use solana_client::rpc_client::RpcClient;
async fn crank_loop(rpc: &RpcClient, program_id: Pubkey, duration_seconds: u64) {
loop {
let clock = rpc.get_clock().unwrap();
let now = clock.unix_timestamp as u64;
// Look back far enough to catch unresolved markets — cover the
// full 7-day expiration grace window (604,800 s).
let lookback = 604_800 / duration_seconds;
let current_epoch = now / duration_seconds;
for offset in 0..=lookback {
let market_id = current_epoch.saturating_sub(offset);
let t_start = market_id * duration_seconds;
let t_end = t_start + duration_seconds;
let market_pda = derive_market_pda(market_id, &program_id);
match rpc.get_account(&market_pda) {
Err(_) => {
// Market doesn't exist yet
if now >= t_start {
send_create_market(market_id).await;
}
}
Ok(account) => {
let market = MarketAccount::deserialize(&account.data).unwrap();
if market.end_price == 0 && now >= t_end {
send_snapshot_end(market_id).await;
} else if market.outcome == 0 && market.end_price != 0 {
send_resolve_market(market_id).await;
} else if market.outcome == 0
&& now > t_end + (7 * 24 * 3600) // MARKET_EXPIRY_WINDOW
{
send_expire_market(market_id).await;
}
}
}
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
Note: redeem is not a crank operation. Traders trigger their own Redeem
— this is by design so that payouts go to the position owner, not to whoever
happens to run a crank.
Pull-oracle markets#
For pull-mode markets (oracle_mode = 1), the SnapshotEnd crank requires
an additional step: fetching the exact first post-boundary price from Pyth's
Hermes API and posting it on-chain as a PriceUpdateV2 account. The SDK's
resolveSnapshotEnd helper bundles this automatically.
Pull mode makes Sampling Rule A firstness deterministic — Hermes can serve the exact first post-boundary print, avoiding the common race condition in push mode where the persistent feed account already advanced past the first valid sample by the time the crank reads it.
Automated trading bots#
Architecture overview#
Reading market state#
| Data | Source | Refresh |
|---|---|---|
| Market state and prices | Solana RPC getAccountInfo | Poll or WebSocket |
| Order book (bids + asks) | Solana RPC getAccountInfo on orderbook PDA | Poll or WebSocket |
| Your position | Solana RPC getAccountInfo on position PDA | After each trade |
| Oracle price | Pyth WebSocket / Hermes | ~400 ms |
Use WebSocket subscriptions (accountSubscribe) for latency-sensitive
strategies. Use polling as a fallback or for lower-frequency strategies.
Free-funds trading#
For high-frequency strategies, use the *WithFreeFunds instruction variants.
These operate on an internal trader-ledger balance instead of performing
SPL token transfers on every order:
DepositFunds → deposit stablecoin into quote_free (your ledger balance)
SwapWithFreeFunds → IOC order settled against ledger (no per-order token CPI)
PlaceLimitOrderWithFreeFunds → Limit/PostOnly settled against ledger
CancelMultipleOrdersByIdWithFreeFunds → cancel + refund to ledger
WithdrawFunds → withdraw quote_free back to your wallet
Free-funds orders reduce transaction cost and latency for bots that cycle rapidly through the same capital.
Order lifecycle state tracking#
Order states:
Active → resting on the book, waiting to fill
PartiallyFilled → some quantity matched, remainder resting
Filled → fully matched, slot can be freed
Cancelled → you cancelled it
ExpiredClaimable → TTL elapsed; permissionless ReclaimExpiredOrder available
Track your orders by order_id. After each transaction, re-read the
orderbook PDA to confirm actual state.
Pre-trade checks#
Always validate before sending:
| Check | How |
|---|---|
| Market in TRADING state | market.state == Trading |
Not too close to t_end | now < market.t_end − safety_margin |
| Sufficient USDT balance | Compare user_token_account.amount to required collateral |
| Book not full (63 per side) | Count active slots before posting |
| Price inside maker band | Compute band from best bid/ask before posting |
Retry and error handling#
| Error | Retryable | Recommended action |
|---|---|---|
OrderPriceOutOfBand | Yes (re-price) | Re-read book, recalculate quotes |
OrderbookFull | Yes (wait) | Back off, cancel a stale order |
TradingEnded | No | Stop placing orders; switch to settlement |
InsufficientBalance | No | Check actual on-chain balance |
| Network / RPC timeout | Yes | Exponential backoff, switch endpoint |
| Blockhash expired | Yes | Re-fetch recent blockhash, resend |
Settlement automation#
Bots should automate the claim step too, not just order placement:
Critical: automate claims or you risk the 7-day forfeiture window. A bot that places orders but doesn't claim is leaving money behind. See Claiming Winnings and Risks.
Strategy patterns#
Momentum#
Track Pyth oracle price movements and trade in the direction of recent momentum:
Market making#
See Market Making for the full two-sided quoting strategy. The key loop for a maker bot:
- Read book → compute mid → compute bid/ask
- Cancel stale quotes
- Place new quotes via
PlaceMultiplePostOnlyOrders - Monitor fills → adjust inventory skew
- Cancel all before
t_end − safety_margin
Arbitrage#
When YES + NO < 10000 bps (both available cheaper than $1 total): buying both locks in a guaranteed profit at settlement. Always net against the taker fee before acting.
Priority fees#
During network congestion, add compute-budget priority fees:
| Congestion | Priority fee |
|---|---|
| Low | 0 |
| Medium | 10,000 µlam/CU |
| High | 100,000 µlam/CU |
Also prepend RequestHeapFrame(64 KiB) for PlaceOrder on a full book —
the place-order hot path can exceed Solana's default 32 KiB heap.
Operational considerations#
Key management#
| Risk | Mitigation |
|---|---|
| Key exposure | Store in environment variables or a hardware vault |
| Key compromise | Keep minimal SOL on the bot wallet; top up as needed |
| Unauthorized access | IP allowlisting; separate bot wallet from main wallet |
Uptime#
The Seesaw protocol operates continuously. Market lifecycles run 24/7 via permissionless cranks. Your bot should handle:
- RPC node downtime → failover endpoint
- Websocket disconnects → reconnect with state re-sync
- Program errors → circuit breaker to pause and alert
Testing#
- Devnet first. Run your strategy with devnet USDT before mainnet.
- Paper trading. Log intended orders without sending them to verify logic.
- Small capital. Ramp up gradually: 1% → 10% → full capital.
Next steps#
- Market Making — the two-sided quoting strategy in detail
- Placing Orders — all order types, free-funds variants, TTL
- Risks — admin controls (pause/post-only) that affect bots
- SDK Guide — TypeScript, Python, and Rust SDK references
Technical reference#
| Mechanism | Instruction |
|---|---|
| Create market + capture start price | CreateMarket (0x03) |
| Capture end price | SnapshotEnd (0x04) |
| Resolve outcome | ResolveMarket (0x05) |
| Oracle-outage fallback | ExpireMarket (0x06) |
| Deposit to ledger balance | DepositFunds (0x08) |
| Withdraw from ledger balance | WithdrawFunds (0x09) |
| IOC with free funds | SwapWithFreeFunds (0x0D) |
| Limit/PostOnly with free funds | PlaceLimitOrderWithFreeFunds (0x0E) |
| Batch post-only with free funds | PlaceMultiplePostOnlyOrdersWithFreeFunds (0x0F) |
| Cancel with ledger refund | CancelMultipleOrdersByIdWithFreeFunds (0x15) |
| Reclaim expired order (+ optional bounty) | ReclaimExpiredOrder (0x19) |
| Claim winnings | Redeem (0x1A) |
| Permissionless position settle | MarkPositionSettled (0x1C) |
| Close position PDA | ClosePosition (0x1D) |
| Market teardown | CloseMarket (0x1E) |