Seesaw Trading Skill#
This skill provides comprehensive guidance for autonomous trading on Seesaw, a permissionless binary prediction market protocol on Solana.
Protocol Overview#
Seesaw enables trading on configurable-duration binary outcomes: will an asset's price end UP or DOWN relative to its starting price? Users trade YES/NO shares through an on-chain order book with Pyth oracle integration.
Key Characteristics#
- Configurable durations: Markets run from 60 seconds to 7 days (default: 15 minutes)
- Binary outcomes: UP (price increased or equal) or DOWN (price decreased)
- Permissionless: Anyone can trade, create markets, or run cranks
- Pyth Oracle: Exclusive use of Pyth Network for price data
- Basis points pricing: All prices in bps [0, 10000] where 10000 = 100%
Market Lifecycle#
PENDING → CREATED → TRADING → SETTLING → RESOLVED → CLOSED
Lifecycle Stages#
| Stage | Description | User Actions |
|---|---|---|
| PENDING | Market not yet created | Can call create_market |
| CREATED | Market exists, awaiting start | Wait for t_start |
| TRADING | Active trading window | place_order, cancel_order |
| SETTLING | Trading ended, awaiting resolution | Wait for snapshots |
| RESOLVED | Outcome determined (UP/DOWN) | Call redeem |
| CLOSED | All positions settled | Market complete |
Time Calculations#
// docs-check: semantic
// Calculate market boundaries from any timestamp
const EPOCH_DURATION = 900; // default 15 minutes; configurable per market (60–604800)
function getMarketId(timestamp: number): bigint {
return BigInt(Math.floor(timestamp / EPOCH_DURATION));
}
function getMarketTimes(marketId: bigint) {
const tStart = Number(marketId) * EPOCH_DURATION;
const tEnd = tStart + EPOCH_DURATION;
return { tStart, tEnd };
}
// Example: Get current market
const now = Math.floor(Date.now() / 1000);
const currentMarketId = getMarketId(now);
const { tStart, tEnd } = getMarketTimes(currentMarketId);
Fetching Price Data#
Pyth Network Integration#
Seesaw uses Pyth Network exclusively for price feeds. Use the Hermes SSE streaming API for real-time prices.
Known Feed IDs#
// docs-check: semantic
const PYTH_FEED_IDS = {
'BTC/USD': '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43',
'ETH/USD': '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace',
'SOL/USD': '0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d',
'AVAX/USD': '0x93da3352f9f1d105fdfe4971cfa80e9dd777bfc5d0f683ebb6e1294b92137bb7',
'LINK/USD': '0x8ac0c70fff57e9aefdf5edf44b51d62c2d433653cbb2cf5cc06bb115af04d221',
'ARB/USD': '0x3fa4252848f9f0a1480be62745a4629d9eb1322aebab8a791e344b3b9c1adcf5',
'OP/USD': '0x385f64d993f7b77d8182ed5003d97c60aa3361f3cecfe711544d2d59165e9bdf',
};
Streaming Real-Time Prices#
// docs-check: semantic
const STREAM_FEED_IDS = {
'BTC/USD': '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43',
} as const;
interface PythStreamPriceUpdate {
price: {
price: string;
expo: number;
publish_time: number;
};
}
interface PythStreamMessage {
parsed?: PythStreamPriceUpdate[];
}
function buildPythStreamProxyUrl(feedId: string): string {
const url = new URL('/api/pyth/stream', 'https://api.seesaw.markets');
url.searchParams.set('ids[]', feedId);
return url.toString();
}
// Browser/EventSource paths should use a configured app proxy that injects
// PYTH_API_KEY server-side. Do not hard-code indexer REST aliases here.
const feedId = STREAM_FEED_IDS['BTC/USD'].replace(/^0x/, '');
const url = buildPythStreamProxyUrl(feedId);
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data) as PythStreamMessage;
if (data.parsed && Array.isArray(data.parsed)) {
for (const update of data.parsed) {
const price = parseFloat(update.price.price);
const expo = update.price.expo;
const actualPrice = price * Math.pow(10, expo);
const publishTime = update.price.publish_time;
console.log(`Price: $${actualPrice.toFixed(2)} at ${publishTime}`);
}
}
};
One-Time Price Fetch#
// docs-check: semantic
interface HermesPriceUpdate {
price: {
price: string;
expo: number;
};
}
interface HermesLatestPriceResponse {
parsed?: HermesPriceUpdate[];
}
async function fetchCurrentPrice(feedId: string): Promise<number> {
const id = feedId.replace(/^0x/, '');
const url = `https://pyth.dourolabs.app/hermes/v2/updates/price/latest?ids[]=${id}&parsed=true`;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.PYTH_API_KEY}` },
});
const data = (await response.json()) as HermesLatestPriceResponse;
if (data.parsed && data.parsed.length > 0) {
const update = data.parsed[0];
const price = parseFloat(update.price.price);
const expo = update.price.expo;
return price * Math.pow(10, expo);
}
throw new Error('No price data available');
}
Order Book Mechanics#
Understanding YES/NO Share Pricing#
Seesaw uses a single canonical order book for YES shares. NO orders are automatically converted:
| User Intent | User Price | Canonical Side | Canonical Price |
|---|---|---|---|
| Buy YES @ 6000 | 6000 bps | BID | 6000 |
| Sell YES @ 6000 | 6000 bps | ASK | 6000 |
| Buy NO @ 4000 | 4000 bps | ASK | 6000 (10000 - 4000) |
| Sell NO @ 4000 | 4000 bps | BID | 6000 (10000 - 4000) |
Key insight: Buying NO at 40% is equivalent to selling YES at 60%.
Price Interpretation#
Price in bps → Implied probability
6000 bps = 60% probability of UP
4000 bps = 40% probability of UP (or 60% probability of DOWN)
Collateral calculation:
collateral = (price_bps * quantity) / 10000
Example: Buy 100 YES shares at 6000 bps
collateral = (6000 * 100) / 10000 = 60 USDT
Tick Rounding#
Orders are rounded to tick boundaries (default: 100 bps):
- Bids: Round DOWN (buyer gets better price)
- Asks: Round UP (seller gets better price)
// docs-check: semantic
function roundToTick(price: number, tickSize: number, side: 'bid' | 'ask'): number {
if (side === 'bid') {
return Math.floor(price / tickSize) * tickSize;
} else {
const remainder = price % tickSize;
return remainder > 0 ? price + (tickSize - remainder) : price;
}
}
// Examples:
roundToTick(6050, 100, 'bid'); // → 6000
roundToTick(6050, 100, 'ask'); // → 6100
Trading Instructions#
1. Place Order#
Place a limit order on the order book.
// docs-check: semantic
import type { Address } from '@solana/addresses';
import { OrderSide, OrderType } from '@seesaw/core';
interface PlaceOrderParams {
market: Address; // Market account PDA
side: OrderSide; // 0=BuyYes, 1=SellYes, 2=BuyNo, 3=SellNo
priceBps: number; // Price in basis points [1, 9999]
quantity: bigint; // Number of shares
orderType: OrderType; // 0=Limit, 1=PostOnly, 2=ImmediateOrCancel
}
Base accounts (18, before market-specific and recorder tails):
market- Market PDAorderbook- Orderbook PDAuser_position- User's position PDA (created if needed)user_token_account- User's USDT accountvault- Market vault PDAuser- User's wallet (signer)config- Protocol config PDAtreasury_token_account- Must be a config-approved recipient; the supplied recipient determines the effective treasury index- Plus: token/system programs, settlement mint, YES/NO escrows, user YES/NO ATAs, YES/NO mints, trader ledger
A spline-enabled market requires its single writable creator spline after the
base accounts. Next comes an optional single read-only ReferralAccount, then
the mandatory [self_program, log_authority] recorder pair. Total: 20–21 accounts
without a spline, 21–22 with one. The old three-account referral triple is not
accepted by the current native ABI.
Use the SDK builders (buildPlaceOrderIx in @seesaw/core, place_order in
the Python/Rust SDKs) with accounts your application has derived and validated.
There is no automatic self-custody account resolver; see Reading data.
2. Cancel Order#
Remove an open order from the book.
// docs-check: semantic
import type { Address } from '@solana/addresses';
interface CancelOrderParams {
market: Address;
orderId: bigint; // The order's unique u64 id (from placement or the on-chain book)
}
3. Redeem#
Claim winnings after market resolution by burning winning tokens.
// docs-check: semantic
import type { Address } from '@solana/addresses';
interface RedeemParams {
market: Address;
amount: bigint; // Amount of tokens to redeem
tokenType: 'yes' | 'no'; // Which token to redeem (0=Yes, 1=No)
}
// Payout calculation:
// if outcome == UP: burn YES tokens → receive USDT 1:1
// if outcome == DOWN: burn NO tokens → receive USDT 1:1
// if outcome == EXPIRED: burn either → receive USDT at 50%
Additional Instructions#
4. Mint Shares (0x07)#
Mint YES/NO share pairs by depositing USDT collateral.
5. Withdraw Shares (0x0A)#
Withdraw trader-ledger position shares as SPL tokens.
6. Snapshot End (0x04)#
Capture the end price snapshot after trading ends. Permissionless.
7. Resolve Market (0x05)#
Determine market outcome based on price snapshots. Permissionless.
8. Expire Market (0x06)#
Expire a market that hasn't been resolved within the expiration window.
9. Close Market (0x1E)#
Close a resolved/expired market and reclaim rent.
10. Force Close (0x1B)#
Force-close a position in a resolved/expired market.
11. Create Market (0x03)#
Create a new prediction market for a given asset and duration.
12. Initialize Config (0x00)#
One-time protocol configuration setup (tick_size_bps, plus the 8 protocol treasury recipient accounts and the default settlement mint). The capped-linear-decay fee curve (fee_cap_bps, decay_rate_bps) and fee split (protocol_fee_bps, maker_rebate_share_bps, default_creator_fee_bps, referral_share_bps_of_fee) initialize to protocol defaults and are retuned via UpdateFeeConfig (0x1F).
13. Claim Creator Fees (0x23)#
Permissionless sweep of deferred creator fees from the market vault to the market creator's token account.
14. Update Fee Config (0x1F)#
Admin-only. Retunes the fee curve parameters and the four-way allocation and maker resting-age gate. Preserves the invariant protocol + creator + referral + maker == 10_000 (bps of fee).
15. Set Referrer (0x21)#
Permissionless. Writes a ReferralAccount PDA attributing a referrer to the user. First-touch and immutable after creation; expiry only stops future referral accrual and does not allow picking a new referrer. While active, eligible taker fees accrue the configured referral share (5% at shipped defaults) as a deferred market liability when the valid referral triple is attached; rollup later credits the referrer's earnings.
16. Init Referrer Earnings Account (0x22)#
Permissionless. Creates the per-referrer ReferrerEarningsAccount PDA (once, lazily).
17. Claim Referrer Earnings (0x24)#
Permissionless. Transfers the referrer's accumulated earnings from their bound referrer_treasury shard (one of 8 sharded PDAs) to their token account. Funds always go to the referrer regardless of caller.
API Endpoints#
The Seesaw indexer provides REST endpoints for market data. Use the
versioned /api/v1/ prefix.
For an indexer-free alternative — every market, order book, position, and
balance read straight from any Solana RPC node — see the
canonical v1 SDK.
List Markets#
GET /api/v1/markets?state={state}&limit={limit}&offset={offset}
Response:
{
"markets": [
{
"address": "...",
"marketId": "1234567",
"pythFeed": "...",
"state": 1,
"outcome": null,
"tStart": "2024-01-15T10:00:00Z",
"tEnd": "2024-01-15T10:15:00Z",
"startPrice": "68543210000",
"totalVolume": "1000000",
"totalTrades": 42
}
],
"total": 100,
"limit": 50,
"offset": 0
}
Get Current Market#
GET /api/v1/markets/current
Response:
{
"market": {
"id": "BTC-1H-2026-07-04T15:00Z",
"question": "Will BTC close above $110,000?"
},
"status": "trading"
}
Get Market Details#
GET /api/v1/markets/{marketId}
Response includes orderbook:
{
"market": {
"id": "BTC-1H-2026-07-04T15:00Z",
"question": "Will BTC close above $110,000?"
},
"orderbook": {
"bids": [{ "price": 6000, "quantity": "1000", "orders": 3 }],
"asks": [{ "price": 6100, "quantity": "500", "orders": 2 }]
}
}
Trading Strategy Framework#
Decision Factors#
When deciding whether to trade, consider:
- Price Position: Is current price above or below start price?
- Time Remaining: How much time until market ends?
- Momentum: Is price trending in a direction?
- Market Prices: What probabilities does the market imply?
- Confidence: How confident is the oracle price?
Computing Market Position#
// docs-check: semantic
interface MarketPosition {
delta: number; // currentPrice - startPrice
deltaPct: number; // Percentage change
status: 'above' | 'below' | 'at';
distanceToFlip: number;
isAbove: boolean;
isBelow: boolean;
}
function computeMarketPosition(startPrice: number, currentPrice: number): MarketPosition {
const delta = currentPrice - startPrice;
const deltaPct = delta / startPrice;
const status = deltaPct > 0.0001 ? 'above' : deltaPct < -0.0001 ? 'below' : 'at';
return {
delta,
deltaPct,
status,
distanceToFlip: Math.abs(delta),
isAbove: status === 'above',
isBelow: status === 'below',
};
}
const samplePosition = computeMarketPosition(100, 101);
console.log(samplePosition.status, samplePosition.distanceToFlip);
Implied Probability Heuristic#
// docs-check: semantic
interface MarketPosition {
delta: number;
deltaPct: number;
status: 'above' | 'below' | 'at';
distanceToFlip: number;
isAbove: boolean;
isBelow: boolean;
}
function computeImpliedProbability(
position: MarketPosition,
volatilityFactor: number = 0.02
): number {
// Logistic function centered at start price
const k = 1 / volatilityFactor;
const probability = 1 / (1 + Math.exp(-k * position.deltaPct));
return Math.max(0.01, Math.min(0.99, probability));
}
const probability = computeImpliedProbability({
delta: 1,
deltaPct: 0.01,
status: 'above',
distanceToFlip: 1,
isAbove: true,
isBelow: false,
});
console.log(probability);
Trading Decision Logic#
// docs-check: semantic
interface MarketPosition {
delta: number;
deltaPct: number;
status: 'above' | 'below' | 'at';
distanceToFlip: number;
isAbove: boolean;
isBelow: boolean;
}
interface TradingSignal {
action: 'buy_yes' | 'buy_no' | 'hold';
confidence: number;
reason: string;
suggestedPrice: number;
suggestedQuantity: number;
}
function computeMarketPosition(startPrice: number, currentPrice: number): MarketPosition {
const delta = currentPrice - startPrice;
const deltaPct = delta / startPrice;
const status = deltaPct > 0.0001 ? 'above' : deltaPct < -0.0001 ? 'below' : 'at';
return {
delta,
deltaPct,
status,
distanceToFlip: Math.abs(delta),
isAbove: status === 'above',
isBelow: status === 'below',
};
}
function computeImpliedProbability(
position: MarketPosition,
volatilityFactor: number = 0.02
): number {
const k = 1 / volatilityFactor;
const probability = 1 / (1 + Math.exp(-k * position.deltaPct));
return Math.max(0.01, Math.min(0.99, probability));
}
function analyzeTradeOpportunity(
startPrice: number,
currentPrice: number,
marketBestBid: number, // Best YES bid in bps
marketBestAsk: number, // Best YES ask in bps
timeRemainingSeconds: number,
volatilityEstimate: number = 0.02
): TradingSignal {
const position = computeMarketPosition(startPrice, currentPrice);
const impliedProb = computeImpliedProbability(position, volatilityEstimate);
// Convert to basis points
const impliedPriceBps = Math.round(impliedProb * 10000);
// Market mid price
const marketMid = (marketBestBid + marketBestAsk) / 2;
// Look for mispricing
const priceDiff = impliedPriceBps - marketMid;
const threshold = 200; // 2% edge threshold
// Time decay factor - less aggressive near end
const timeFactor = Math.max(0.3, timeRemainingSeconds / 900);
if (priceDiff > threshold * timeFactor) {
// Market underpricing YES (or overpricing NO)
// Buy YES below our fair value
return {
action: 'buy_yes',
confidence: Math.min(1, Math.abs(priceDiff) / 500),
reason: `YES underpriced by ${priceDiff} bps`,
suggestedPrice: Math.min(impliedPriceBps - 50, marketBestAsk),
suggestedQuantity: calculatePositionSize(priceDiff, timeFactor),
};
} else if (priceDiff < -threshold * timeFactor) {
// Market overpricing YES (or underpricing NO)
// Buy NO
return {
action: 'buy_no',
confidence: Math.min(1, Math.abs(priceDiff) / 500),
reason: `NO underpriced by ${-priceDiff} bps`,
suggestedPrice: 10000 - Math.max(impliedPriceBps + 50, marketBestBid),
suggestedQuantity: calculatePositionSize(-priceDiff, timeFactor),
};
}
return {
action: 'hold',
confidence: 0,
reason: 'No significant mispricing detected',
suggestedPrice: 0,
suggestedQuantity: 0,
};
}
function calculatePositionSize(edgeBps: number, timeFactor: number): number {
// Kelly-inspired sizing: bet more when edge is larger
// Scale down near market end (timeFactor)
const baseSize = 100; // Base position in shares
const edgeFactor = Math.min(3, Math.abs(edgeBps) / 100);
return Math.floor(baseSize * edgeFactor * timeFactor);
}
const signal = analyzeTradeOpportunity(100, 101, 5_800, 6_100, 600);
console.log(signal.action, signal.suggestedQuantity);
Fee Structure#
Seesaw uses a capped-linear-decay taker fee curve. Makers pay 0; eligible fills also earn rebates. The shipped allocation is 50% protocol / 5% creator / 5% referral / 40% maker rebate. Eligible maker fills credit the maker's free quote balance; ineligible maker allocation and rounding dust go to protocol, as does the referral allocation without an eligible referrer. See fee constants and allocation details.
<!-- src: program/src/logic/fee.rs:168 FeeSplit4::TARGET --> <!-- F-01: update with builder fee allocation -->fee_bps(p) = min(fee_cap_bps, decay_rate_bps × (10_000 − p) / 10_000)
Shipped defaults: fee_cap_bps = 200 (2.00% cap), decay_rate_bps = 400 (4.00% slope).
Read deployed config for current settings. Rate division rounds down; amounts round up.
| Fill price | Effective taker fee |
|---|---|
| 0.00 - 0.50 | 2.00% (capped) |
| 0.80 | 0.80% |
| 0.90 | 0.40% |
| 0.95 | 0.20% |
| 0.99 | 0.04% |
Four-Way Allocation#
Configured shares are bps of fee and sum to 10_000:
- Protocol treasury: 50% (5_000 bps of fee)
- Market creator: 5% (500 bps of fee)
- Referral allocation: 5% (500 bps of fee) — deferred for an eligible referrer, else protocol
- Maker allocation: 40% (4_000 bps of fee) — free quote balance on eligible fills, else protocol
// docs-check: semantic
import { computeAndSplit } from '@seesaw/core';
function calculateFees(
notional: bigint,
priceBps: number,
isTaker: boolean,
hasReferrer: boolean,
feeCapBps: number = 200,
decayRateBps: number = 400
): {
total: bigint;
protocolFee: bigint;
creatorFee: bigint;
referralFee: bigint;
} {
if (!isTaker) {
return { total: 0n, protocolFee: 0n, creatorFee: 0n, referralFee: 0n };
}
// Single-fill illustration with no eligible maker rebate (makerTotal = 0n).
// Pass actual credited maker totals for eligible fills; unused maker share
// remains with protocol. Multi-fill fees must be calculated per fill.
// Matches on-chain rounding: total fee rounds up, creator/referral shares
// round down, and all dust stays with protocol.
const split = computeAndSplit(
notional,
priceBps,
feeCapBps,
decayRateBps,
{ protocolBps: 5000, creatorBps: 500, referralBps: 500, makerBps: 4000 },
0n
);
// If taker has no eligible referrer, the referral share flows to protocol.
const protocolFee = hasReferrer ? split.protocol : split.protocol + split.referral;
const referralFee = hasReferrer ? split.referral : 0n;
return {
total: split.total,
protocolFee,
creatorFee: split.creator,
referralFee,
};
}
Resolution Rules#
Sampling Rule A#
P_start= First Pyth price wherepublish_time >= t_startP_end= First Pyth price wherepublish_time >= t_end
Outcome Determination#
Important: Equality results in UP outcome.
Payout Calculation#
PDA Derivation#
// docs-check: semantic
import { address } from '@solana/addresses';
import {
deriveMarketPda,
deriveOrderbookPda,
deriveVaultPda,
derivePositionPda,
deriveConfigPda,
deriveAssetPda,
deriveYesMintPda,
deriveNoMintPda,
} from '@seesaw/core';
const pythFeedId = new Uint8Array(32);
const durationSeconds = 900n;
const marketId = 1n;
const creatorAddress = address('11111111111111111111111111111111');
const userAddress = address('SysvarC1ock11111111111111111111111111111111');
// Market PDA uses 6 seeds:
// ["seesaw", "market", pyth_feed_id(32), duration_seconds(8 LE), market_id(8 LE), creator(32)]
const [marketPda] = await deriveMarketPda(pythFeedId, durationSeconds, marketId, creatorAddress);
const [orderbookPda] = await deriveOrderbookPda(marketPda);
const [vaultPda] = await deriveVaultPda(marketPda);
const [positionPda] = await derivePositionPda(marketPda, userAddress);
// Additional PDA derivations
const [configPda] = await deriveConfigPda();
const [assetPda] = await deriveAssetPda(pythFeedId);
const [yesMintPda] = await deriveYesMintPda(marketPda);
const [noMintPda] = await deriveNoMintPda(marketPda);
console.log({ orderbookPda, vaultPda, positionPda, configPda, assetPda, yesMintPda, noMintPda });
Risk Management#
Position Limits#
- Maximum order size:
max_order_size(default 1,000,000,000,000 base units = 1,000,000 USDT; admin-tunable viaUpdateOperationalParams0x2F) - No per-user order limit: an earlier
max_orders_per_userfield was retired and is no longer enforced - Maximum orderbook depth: selected deep orderbook tier, 64-4096 bids plus 64-4096 asks
Key Invariants (Never Violated)#
- Solvency:
vault >= max(total_yes_shares, total_no_shares) + accumulated_creator_fees - Conservation: Every trade conserves value (collateral in = shares out)
- No Negative Exposure: All share balances >= 0
- No Crossed Book:
best_bid < best_askwhen both exist
Error Handling#
Common errors to handle:
Errors are a custom #[repr(u32)] SeesawError enum (category-prefixed hex codes, NOT
Anchor 6000+). Representative codes:
| Error Code | Name | Meaning |
|---|---|---|
| 0x1002 | InvalidState | Market not in the expected state |
| 0x1005 | AlreadyResolved | Market already resolved |
| 0x2002 | StaleOracle | Oracle price too old |
| 0x3001 | InvalidQuantity | Order quantity invalid |
| 0x3004 | WouldCross | PostOnly order would cross book |
| 0x3007 | SlippageExceeded | Fill price outside tolerance |
| 0x4001 | MathOverflow | Arithmetic overflow |
| 0x6001 | ProtocolPaused | Protocol is paused |
Complete Trading Flow Example#
// docs-check: semantic
interface TradingFlowMarket {
address: string;
pythFeed: string;
startPrice: string;
startPriceExpo: number;
tEnd: string;
}
interface TradingFlowOrderbook {
bids: Array<{ price: number }>;
asks: Array<{ price: number }>;
}
interface TradingSignal {
action: 'buy_yes' | 'buy_no' | 'hold';
confidence: number;
reason: string;
suggestedPrice: number;
suggestedQuantity: number;
}
interface PythLatestPriceResponse {
parsed: Array<{
price: {
price: string;
expo: number;
};
}>;
}
function analyzeTradeOpportunity(
startPrice: number,
currentPrice: number,
bestBid: number,
bestAsk: number,
timeRemainingSeconds: number
): TradingSignal {
const midpoint = Math.floor((bestBid + bestAsk) / 2);
if (timeRemainingSeconds < 60) {
return {
action: 'hold',
confidence: 0.2,
reason: 'market is near expiry',
suggestedPrice: midpoint,
suggestedQuantity: 0,
};
}
return {
action: currentPrice >= startPrice ? 'buy_yes' : 'buy_no',
confidence: 0.6,
reason:
currentPrice >= startPrice ? 'current price is above start' : 'current price is below start',
suggestedPrice: midpoint,
suggestedQuantity: 1_000_000,
};
}
async function executeTradingStrategy() {
// 1. Get current market
const marketResponse = await fetch('/api/v1/markets/current');
const { market, status } = (await marketResponse.json()) as {
market: TradingFlowMarket;
status: string;
};
if (status !== 'trading') {
console.log('No active market, waiting...');
return;
}
// 2. Stream live price
const feedId = market.pythFeed.replace(/^0x/, '');
const priceUrl = `https://pyth.dourolabs.app/hermes/v2/updates/price/latest?ids[]=${feedId}&parsed=true`;
const priceResponse = await fetch(priceUrl, {
headers: { Authorization: `Bearer ${process.env.PYTH_API_KEY}` },
});
const priceData = (await priceResponse.json()) as PythLatestPriceResponse;
const currentPrice =
parseFloat(priceData.parsed[0].price.price) * Math.pow(10, priceData.parsed[0].price.expo);
// 3. Parse market data
const startPrice = Number(market.startPrice) * Math.pow(10, market.startPriceExpo);
const tEnd = new Date(market.tEnd).getTime() / 1000;
const now = Date.now() / 1000;
const timeRemaining = tEnd - now;
// 4. Get orderbook
const obResponse = await fetch(`/api/v1/markets/${market.address}`);
const { orderbook } = (await obResponse.json()) as { orderbook: TradingFlowOrderbook };
const bestBid = orderbook.bids[0]?.price || 0;
const bestAsk = orderbook.asks[0]?.price || 10000;
// 5. Analyze opportunity
const signal = analyzeTradeOpportunity(startPrice, currentPrice, bestBid, bestAsk, timeRemaining);
console.log('Trading signal:', signal);
// 6. Execute if confident
if (signal.action !== 'hold' && signal.confidence > 0.5) {
// Build and send transaction...
console.log(`Executing: ${signal.action} at ${signal.suggestedPrice} bps`);
}
}
// Run every 30 seconds during active markets
const strategyTimer = setInterval(() => void executeTradingStrategy(), 30_000);
clearInterval(strategyTimer);
Instruction Discriminators#
| Discriminator | Instruction | Description |
|---|---|---|
| 0x00 | InitializeConfig | One-time protocol setup |
| 0x01 | UpdateAuthority | Name pending protocol authority |
| 0x02 | ClaimAuthority | Pending authority accepts control |
| 0x03 | CreateMarket | Create a new prediction market |
| 0x04 | SnapshotEnd | Capture end price after trading window |
| 0x05 | ResolveMarket | Determine outcome from price snapshots |
| 0x06 | ExpireMarket | Late-resolve or expire after timeout |
| 0x07 | MintShares | Mint YES/NO share pairs from USDT |
| 0x08 | DepositFunds | Deposit stablecoin into trader ledger |
| 0x09 | WithdrawFunds | Withdraw stablecoin from trader ledger |
| 0x0A | WithdrawShares | Withdraw shares as SPL tokens |
| 0x0B | PlaceOrder | Place order on the order book |
| 0x0C | PlaceMultiplePostOnlyOrders | Batch post-only placement |
| 0x0D | SwapWithFreeFunds | IOC trade using ledger free funds |
| 0x0E | PlaceLimitOrderWithFreeFunds | Limit/PostOnly using ledger free funds |
| 0x0F | PlaceMultiplePostOnlyOrdersWithFreeFunds | Batch post-only using ledger free funds |
| 0x10 | CancelOrder | Cancel an open order |
| 0x11 | CancelMultipleOrdersById | Bulk cancel explicit order IDs |
| 0x12 | CancelAllOrders | Cancel all caller-owned orders |
| 0x13 | CancelUpTo | Threshold cancel |
| 0x14 | ReduceOrder | Reduce a resting order |
| 0x15 | CancelMultipleOrdersByIdWithFreeFunds | Bulk cancel to ledger free funds |
| 0x16 | CancelAllOrdersWithFreeFunds | Cancel all to ledger free funds |
| 0x17 | CancelUpToWithFreeFunds | Threshold cancel to ledger free funds |
| 0x18 | ReduceOrderWithFreeFunds | Reduce order to ledger free funds |
| 0x19 | ReclaimExpiredOrder | Reclaim expired resting order |
| 0x1A | Redeem | Burn winning tokens for USDT |
| 0x1B | ForceClose | Force-close expired position |
| 0x1C | MarkPositionSettled | Mark empty position settled |
| 0x1D | ClosePosition | Close settled position PDA |
| 0x1E | CloseMarket | Close market and reclaim rent |
| 0x1F | UpdateFeeConfig | Retune curve + four-way allocation (admin) |
| 0x20 | UpdateTreasuryRecipients | Rotate all 8 protocol treasury recipients |
| 0x21 | SetReferrer | Attribute a referrer to a user |
| 0x22 | InitReferrerEarningsAccount | Create per-referrer earnings PDA |
| 0x23 | ClaimCreatorFees | Sweep deferred creator fees |
| 0x24 | ClaimReferrerEarnings | Sweep referrer earnings to referrer's ATA |
| 0x25 | Pause | Pause protocol trading |
| 0x26 | Unpause | Resume protocol trading |
| 0x27 | EnablePostOnlyMode | Enable protocol-wide PostOnly mode |
| 0x28 | DisablePostOnlyMode | Disable protocol-wide PostOnly mode |
| 0x29 | UpdateTickSize | Update order-book tick size |
| 0x2A | UpdateMinRestingNotional | Update minimum resting notional |
| 0x2B | UpdateMarketCap | Update market max total shares |
| 0x2C | SetMarketEmergencyStatus | Rotate market emergency status |
| 0x2D | ForceCancelMarketOrders | Admin force-cancel market order |
| 0x2E | EnsureTraderLedgerSpace | Allocate or grow trader ledger PDA |
| 0x2F | UpdateOperationalParams | Tune post-deploy operational parameters |
| 0x30 | RecoverSpline | Creator-authorized unresolved spline exit |
| 0x31–0x32 | Reserved | InvalidInstructionData; no usable ABI |
| 0x33 | EnsureDeepOrderbookSpace | Allocate or grow deep orderbook PDA |
| 0x34 | SetPauser | Configure escalation-only pauser authority |
| 0x35 | TopUpCloserRewards | Fund closer reward budget shortfall |
| 0x36 | RollupReferralFees | Roll position referral liabilities into global earnings |
| 0x37 | InitializeReferrerTreasuryShard | Provision a canonical referrer treasury shard |
| 0x38–0x3F | Frozen legacy Reclaim bridge | Permanent tombstones; SDK builders removed |
| 0x40 | InitSpline | Initialize the creator spline |
| 0x41 | UpdateSplineShape | Update the compiled spline level shape |
| 0x42 | UpdateSplineMid | Update spline mid price; eventless |
| 0x43 | AttachSpline | Attach the canonical creator spline |
| 0x44 | SettleSpline | Settle the spline against resolved market |
| 0x45 | DepositSplineVault | Deposit spline collateral |
| 0x46 | WithdrawSplineVault | Withdraw available spline collateral |
| 0x47 | CloseSpline | Close a fully settled spline |
| 0x48 | InitResolverRegistry | Initialize resolver registry; eventless |
| 0x49 | UpdateResolverRegistry | Update resolver-registry governance |
| 0x4A–0x4C | Frozen legacy external lifecycle | Permanent tombstones; SDK builders removed |
| 0x4D | ExpireExternalMarket | Permissionlessly expire an unresolved external market |
| 0x4E | ShrinkPreallocatedChild | Shrink an inert overgrown child; eventless |
| 0x4F | Frozen legacy external preallocation | Permanent tombstone; SDK builder removed |
| 0x50 | ReclaimExternalMarketPreallocation | Return unconsumed preparation to receipt creator |
| 0x51 | TopUpExternalProgressReserve | Restore unpaid lifecycle-progress reserve; eventless |
| 0x52 | BeginReclaimExternalMarketV1 | Begin policy-bound external preallocation |
| 0x53 | OpenReclaimExternalMarketV1 | Open after standalone-verifier definition proof |
| 0x54 | ResolveExternalMarketWithReclaimV1 | Verify, consume, and resolve atomically through CPI |
| 0x55 | HaltExternalMarketWithReclaimV1 | Verify, consume, and halt atomically through CPI |
| 0x56 | ExtendExternalCloseWithReclaimV1 | Verify, consume, and extend atomically through CPI |
The current native ABI has 73 live public instructions. The frozen ranges in
the table are permanent tombstones, rejected with FrozenLegacyReclaim before
parsing or account access. Internal LOG (0xFF) is excluded. All live public
routes require the recorder pair except the five eventless tags 0x33, 0x42,
0x48, 0x4E, and 0x51; see account-list conventions.
Quick Reference#
When to Trade#
BUY YES when:
- Price is above start AND market underpricing YES
- Strong upward momentum with time remaining
- Market ask price < your fair value estimate
BUY NO when:
- Price is below start AND market underpricing NO
- Strong downward momentum with time remaining
- Market bid price > your fair value estimate
HOLD when:
- No significant mispricing
- Very little time remaining (< 1 minute)
- High oracle confidence interval
- Already at position limits