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 privileged keeper or operator. Every lifecycle step is a public instruction that anyone can call. Native markets use an off-chain cranker to fetch a signed Pyth update, post it through the pinned Pyth Receiver, and pass the resulting account into Seesaw. 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 program/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.outcome == 0
&& market.end_price == 0
&& now >= t_end + (7 * 24 * 3600) // MARKET_EXPIRY_WINDOW
{
// For Pull markets, this still needs a Receiver-owned
// PriceUpdateV2 account for the market feed.
send_expire_market(market_id).await;
} else 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;
}
}
}
}
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 native markets, 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 makes Sampling Rule A firstness deterministic — Hermes can serve the exact first post-boundary print before the crank submits the lifecycle instruction.
Seesaw's default Pull keeper is open source in
services/pyth-keeper, and a
public operator guide is available at Run A Pyth Pull Keeper.
It is not privileged: any caller with a funded Solana fee payer and a valid
Pyth update account can submit the same snapshot_end, resolve_market, or
expire_market instructions. Production operators should monitor both the
keeper process and indexed market state; a running process alone does not prove
markets are advancing.
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 | Count active slots against the market's configured capacity |
| 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) |