Skip to main content

Cranking

Running crank infrastructure for the Seesaw protocol.

Overview

Cranks are permissionless operators that execute protocol lifecycle transitions. Anyone can run a crank and earn rewards for keeping the protocol operational.

Market Lifecycle

Epoch Timeline

Epoch N (market_id = M)
├─────────────────────────────────────────────────────────────────┤
│                 duration_seconds (configurable)                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
t_start = M × D                                         t_end = (M+1) × D
│                                                                  │
▼                                                                  ▼
CREATE ──► SNAPSHOT_START ──► TRADING ──► SNAPSHOT_END ──► RESOLVE

Crank Operations

OperationTriggerReward
create_markett >= t_start - 3000.001 SOL
snapshot_startt >= t_start AND valid Pyth price0.001 SOL
snapshot_endt >= t_end AND valid Pyth price0.001 SOL
resolve_marketBoth snapshots captured0.001 SOL
settle_positionMarket resolved, per user0.001 SOL
close_marketAll positions settled0.001 SOL

Setup Guide

Prerequisites

# Node.js 18+
node --version  # v18.x.x

# Solana CLI (must match CI version - see .github/workflows/test.yml)
solana --version  # v3.1.8

# Funded wallet (keypair)
solana balance /path/to/keypair.json

Installation

# Install crank package
pnpm add -g @seesaw/crank

# Or clone and build
git clone https://github.com/seesaw-markets/crank
cd crank
pnpm install
pnpm run build

Configuration

Create crank.config.json:

{
  "rpcUrl": "https://api.mainnet-beta.solana.com",
  "wsUrl": "wss://api.mainnet-beta.solana.com",
  "keypairPath": "/path/to/keypair.json",
  "programId": "SEEsawgSrxRsgtKRbaThZFEKrVqX3Y64hDipTWyi8F8",
  "pythProgramId": "FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH",
  "pollIntervalMs": 1000,
  "maxRetries": 3,
  "priorityFeeLamports": 1000
}

Running

# Start crank service
seesaw-crank start --config crank.config.json

# With environment variables
SEESAW_RPC_URL=https://api.mainnet-beta.solana.com \
SEESAW_KEYPAIR=/path/to/keypair.json \
seesaw-crank start

Crank Loop

Main Loop

Market Window Calculation

function getActiveMarketIds(currentTime: number): bigint[] {
  const durationSeconds = 900; // default; iterate over all active durations
  const currentEpoch = BigInt(Math.floor(currentTime / durationSeconds));
  const ids: bigint[] = [];

  // Look back up to 24 hours (96 epochs) for unsettled markets
  for (let offset = 0; offset <= 96; offset++) {
    if (currentEpoch >= BigInt(offset)) {
      ids.push(currentEpoch - BigInt(offset));
    }
  }

  // Look ahead 1 epoch for pre-creation
  ids.push(currentEpoch + 1n);

  return ids;
}

Decision Logic

async function processMarket(marketId: bigint, currentTime: number) {
  const tStart = Number(marketId) * 900;
  const tEnd = tStart + 900;

  const market = await fetchMarket(marketId);

  if (!market) {
    // Market doesn't exist - try to create
    if (currentTime >= tStart - 300) {
      await createMarket(marketId);
    }
    return;
  }

  // Process based on state
  if (market.startPrice === 0 && currentTime >= tStart) {
    await snapshotStart(market);
  } else if (market.endPrice === 0 && currentTime >= tEnd) {
    await snapshotEnd(market);
  } else if (market.outcome === 0 && market.startPrice !== 0 && market.endPrice !== 0) {
    await resolveMarket(market);
  } else if (market.outcome !== 0) {
    await settleAndClose(market);
  }
}

Idempotency

All crank operations are idempotent - safe to execute multiple times.

Idempotency Rules

OperationDuplicate Behavior
create_marketReturns success (no-op)
snapshot_startReturns success (no-op)
snapshot_endReturns success (no-op)
resolve_marketReturns success (no-op)
settle_positionReturns success (no-op)
close_marketReturns success (no-op)

Reward Protection

Rewards are only paid on the first successful execution:

if already_executed {
    // No reward for no-op
    return Ok(());
}

// Transfer reward to cranker
transfer_reward(cranker, config.crank_reward_lamports)?;

Failure Recovery

Failure Modes

FailureImpactRecovery
Crank offlineOperations delayedOther cranks take over
Pyth feed staleSnapshot failsRetry when feed updates
Insufficient SOLTransaction failsTop up wallet
Compute exceededTransaction failsReduce batch size
Network congestionTransaction droppedRetry with higher priority

Retry Logic

async function executeWithRetry(
  instruction: () => TransactionInstruction,
  maxRetries: number = 3
): Promise<string> {
  let lastError: Error | null = null;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await sendTransaction(instruction());
    } catch (e) {
      lastError = e as Error;

      if (!isRetryable(e)) {
        throw e;
      }

      // Exponential backoff
      await sleep(1000 * Math.pow(2, i));
    }
  }

  throw lastError;
}

function isRetryable(error: Error): boolean {
  const message = error.message.toLowerCase();
  return (
    message.includes('blockhash') ||
    message.includes('network') ||
    message.includes('timeout') ||
    message.includes('rate limit')
  );
}

Stuck Market Recovery

function checkMarketHealth(market: Market, currentTime: number): MarketHealth {
  const age = currentTime - market.tStart;

  // Market should resolve within 30 minutes normally
  if (age > 1800 && market.outcome === 0) {
    if (market.startPrice === 0) {
      return MarketHealth.StuckAtCreated;
    }
    if (market.endPrice === 0) {
      return MarketHealth.StuckAtTrading;
    }
    return MarketHealth.StuckAtSettling;
  }

  // Market should close within 24 hours
  if (age > 86400 && !market.isClosed) {
    return MarketHealth.StuckAtResolved;
  }

  return MarketHealth.Healthy;
}

Advanced Configuration

Priority Fees

During network congestion, increase priority fees:

const priorityFee = computeUnits * microLamportsPerCU;

const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
  units: 200_000,
});

const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({
  microLamports: 1_000, // 0.001 lamports per CU
});

Multi-RPC Setup

For reliability, use multiple RPC providers:

const rpcEndpoints = [
  'https://api.mainnet-beta.solana.com',
  'https://solana-mainnet.rpc.extrnode.com',
  'https://rpc.ankr.com/solana',
];

async function sendWithFallback(tx: Transaction): Promise<string> {
  for (const endpoint of rpcEndpoints) {
    try {
      const connection = new Connection(endpoint);
      return await connection.sendTransaction(tx);
    } catch (e) {
      console.warn(`Failed on ${endpoint}, trying next...`);
    }
  }
  throw new Error('All RPC endpoints failed');
}

Batching Settlements

Batch multiple settlements for efficiency:

async function batchSettle(market: Market, positions: Position[]) {
  const BATCH_SIZE = 5;

  for (let i = 0; i < positions.length; i += BATCH_SIZE) {
    const batch = positions.slice(i, i + BATCH_SIZE);

    const tx = new Transaction();
    for (const pos of batch) {
      tx.add(createSettleInstruction(market, pos.owner));
    }

    await sendTransaction(tx);
  }
}

TukTuk Integration

For production deployments, integrate with TukTuk for managed infrastructure:

import { TukTukClient } from '@helium/tuktuk';

async function registerTasks(marketId: bigint) {
  const tStart = Number(marketId) * 900;
  const tEnd = tStart + 900;

  // Schedule create_market
  await tuktuk.scheduleTask({
    instruction: createMarketInstruction(marketId),
    executeAt: tStart - 300,
    reward: 1_000_000,
  });

  // Schedule snapshot_start
  await tuktuk.scheduleTask({
    instruction: snapshotStartInstruction(marketId),
    executeAt: tStart,
    reward: 1_000_000,
  });

  // Schedule snapshot_end
  await tuktuk.scheduleTask({
    instruction: snapshotEndInstruction(marketId),
    executeAt: tEnd,
    reward: 1_000_000,
  });
}

Operational Runbook

Starting a New Crank

  1. Generate keypair: solana-keygen new -o crank.json
  2. Fund with SOL: solana transfer crank.json 1
  3. Configure RPC endpoint
  4. Start crank service
  5. Monitor logs for first successful operations

Handling Stuck Markets

  1. Identify stuck market via monitoring
  2. Check oracle feed status
  3. If oracle healthy: retry crank operations manually
  4. If oracle stale: wait for feed recovery
  5. If past expiry: execute force_close

Treasury Refill

# Check treasury balance
solana balance <treasury_pubkey>

# Transfer from protocol authority
solana transfer <treasury_pubkey> 50 --from authority.json

Next Steps