Skip to main content
This tutorial builds a fully autonomous prediction market bot using the PMX TypeScript SDK. The bot will:
  1. Create crypto price markets on a schedule
  2. Monitor prices and place trades when it detects an edge
  3. Resolve expired markets using actual price data
  4. Redeem winnings and claim creator fees automatically

Prerequisites

npm install @pmx/parimutuel-sdk @solana/web3.js bs58

Setup

import { Connection, PublicKey, Keypair, Transaction } from "@solana/web3.js";
import bs58 from "bs58";
import {
  ParimutuelClient,
  PMX_PROGRAM_ID,
  toRawUsdc,
  fromRawUsdc,
  getImpliedOdds,
} from "@pmx/parimutuel-sdk";

const connection = new Connection(process.env.SOLANA_RPC_URL!, "confirmed");
const client = new ParimutuelClient(connection, new PublicKey(PMX_PROGRAM_ID));
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.AGENT_PRIVATE_KEY!));
const usdcMint = new PublicKey(process.env.USDC_MINT!);

async function signAndSend(tx: Transaction): Promise<string> {
  tx.sign(wallet);
  const sig = await connection.sendRawTransaction(tx.serialize());
  await connection.confirmTransaction(sig, "confirmed");
  return sig;
}

Part 1 — Market Creator

The creator module spins up new markets on a schedule:
const COINS = [
  { symbol: "BTC", cmcId: 1, name: "Bitcoin" },
  { symbol: "ETH", cmcId: 1027, name: "Ethereum" },
  { symbol: "SOL", cmcId: 5426, name: "Solana" },
];

async function fetchPrice(symbol: string): Promise<number> {
  const res = await fetch(
    `https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?symbol=${symbol}`,
    { headers: { "X-CMC_PRO_API_KEY": process.env.COINMARKET_API_KEY! } }
  );
  const data = await res.json();
  return data.data[symbol].quote.USD.price;
}

async function createMarket() {
  // Pick a random coin
  const coin = COINS[Math.floor(Math.random() * COINS.length)];
  const price = await fetchPrice(coin.symbol);

  // Set threshold near current price (round to significant digits)
  const precision = price > 1000 ? 100 : price > 100 ? 10 : price > 1 ? 0.1 : 0.001;
  const threshold = Math.round(price / precision) * precision;

  // Market resolves in 30 minutes
  const durationMin = 15 + Math.floor(Math.random() * 30);  // 15-45 min
  const resolutionTime = Math.floor(Date.now() / 1000) + durationMin * 60;
  const resDate = new Date(resolutionTime * 1000);
  const timeStr = resDate.toLocaleTimeString("en-US", {
    hour: "numeric", minute: "2-digit", timeZone: "UTC",
  }) + " UTC";

  const ticker = coin.symbol.toUpperCase();
  const seedAmount = toRawUsdc(10 + Math.floor(Math.random() * 15));  // $10-$25

  console.log(`Creating: "${ticker} above $${threshold} by ${timeStr}" — seed: $${fromRawUsdc(seedAmount)}`);

  const { createTx, initMintsTx, setMetadataTx, marketId } = await client.createMarketFull({
    wallet: wallet.publicKey,
    title: `${ticker} above $${threshold.toLocaleString()} by ${timeStr}`,
    question: `Will ${coin.name} (${ticker}) be above $${threshold.toLocaleString()} by ${timeStr}?`,
    yesTicker: `${ticker}UP`,
    noTicker: `${ticker}DN`,
    resolutionTime,
    seedAmount,
    tradeFeeBps: 200,
    usdcMint,
    imageUrl: `https://s2.coinmarketcap.com/static/img/coins/128x128/${coin.cmcId}.png`,
  });

  await signAndSend(createTx);
  await signAndSend(initMintsTx);
  if (setMetadataTx) await signAndSend(setMetadataTx);

  console.log(`✓ Created market #${marketId}`);
  return marketId;
}

Part 2 — Trader

The trader monitors active markets, compares market odds to actual price movements, and trades when it sees an edge:
async function tradeMarkets() {
  const markets = await client.listMarkets({ status: "active" });

  for (const market of markets) {
    // Parse the ticker symbol from the title (e.g. "BTC above $95,000...")
    const match = market.title.match(/^(\w+)\s+above\s+\$([0-9,.]+)/i);
    if (!match) continue;

    const symbol = match[1];
    const threshold = parseFloat(match[2].replace(/,/g, ""));

    // Get current price
    let currentPrice: number;
    try {
      currentPrice = await fetchPrice(symbol);
    } catch {
      continue;
    }

    // Calculate expected probability based on price distance
    const pctFromThreshold = (currentPrice - threshold) / threshold;
    const timeLeft = market.resolutionTime - Math.floor(Date.now() / 1000);
    const timeWeight = Math.max(0.1, Math.min(1, timeLeft / 1800));

    // Simple model: if price is above threshold, YES is more likely
    let expectedYes: number;
    if (pctFromThreshold > 0.02) expectedYes = 0.7 + Math.min(0.25, pctFromThreshold * 5);
    else if (pctFromThreshold < -0.02) expectedYes = 0.3 - Math.min(0.25, Math.abs(pctFromThreshold) * 5);
    else expectedYes = 0.5 + pctFromThreshold * 10;

    // As time runs out, become more certain
    if (timeLeft < 300) {
      expectedYes = currentPrice > threshold ? 0.95 : 0.05;
    }

    const marketYes = market.odds.yes;
    const edge = expectedYes - marketYes;

    if (Math.abs(edge) < 0.05) continue;  // need at least 5% edge

    const side = edge > 0 ? "YES" : "NO";
    const amount = toRawUsdc(Math.min(5, 1 + Math.abs(edge) * 10));

    const quote = client.quoteBuy(
      market.yesReserve, market.noReserve,
      amount, side, market.tradeFeeBps, market.seedLiquidity,
    );

    if (quote.priceImpactBps > 500) continue;  // skip if too much slippage

    try {
      const { tx } = await client.buildBuyTx({
        marketId: market.id,
        wallet: wallet.publicKey,
        side, amount, usdcMint,
      });
      await signAndSend(tx);
      console.log(`Traded $${fromRawUsdc(amount)} ${side} on #${market.id} "${market.title}" (edge: ${(edge * 100).toFixed(1)}%, price: $${currentPrice.toFixed(2)})`);
    } catch (err) {
      console.log(`Trade failed on #${market.id}: ${err.message}`);
    }

    await new Promise(r => setTimeout(r, 300));
  }
}

Part 3 — Resolver

The resolver checks for expired markets and determines the winner using historical price data:
async function resolveExpiredMarkets() {
  const expired = await client.getExpiredUnresolved();

  for (const market of expired) {
    // Only resolve markets I created
    if (market.creator !== wallet.publicKey.toBase58()) continue;

    const match = market.title.match(/^(\w+)\s+above\s+\$([0-9,.]+)/i);
    if (!match) {
      console.log(`Can't parse market #${market.id} title — skipping`);
      continue;
    }

    const symbol = match[1];
    const threshold = parseFloat(match[2].replace(/,/g, ""));

    // Get the price at the exact resolution time using CoinMarketCap historical API
    let priceAtResolution: number;
    try {
      const isoTime = new Date(market.resolutionTime * 1000).toISOString();
      const res = await fetch(
        `https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/historical?symbol=${symbol}&time_start=${isoTime}&count=1`,
        { headers: { "X-CMC_PRO_API_KEY": process.env.COINMARKET_API_KEY! } }
      );
      const data = await res.json();
      const quotes = data.data[symbol]?.[0]?.quotes;
      priceAtResolution = quotes?.[0]?.quote?.USD?.price;
    } catch {
      // Fallback to current price if historical isn't available
      priceAtResolution = await fetchPrice(symbol);
    }

    const winningSide = priceAtResolution > threshold ? "YES" : "NO";
    console.log(`Resolving #${market.id}: ${symbol} was $${priceAtResolution.toFixed(2)} (threshold: $${threshold}) → ${winningSide}`);

    try {
      const tx = await client.buildResolveTx({
        marketId: market.id,
        wallet: wallet.publicKey,
        winningSide,
        usdcMint,
      });
      await signAndSend(tx);
      console.log(`✓ Resolved market #${market.id}${winningSide}`);
    } catch (err) {
      console.log(`✗ Resolution failed: ${err.message}`);
    }

    await new Promise(r => setTimeout(r, 500));
  }
}

Part 4 — Redeemer & Fee Claimer

Collect all value from resolved markets:
async function collectWinnings() {
  // Redeem winning positions
  const positions = await client.getWalletPositions(wallet.publicKey);
  const redeemable = positions.filter(p => p.needsRedemption);

  for (const pos of redeemable) {
    try {
      const tx = await client.buildRedeemTx({
        marketId: pos.marketId,
        wallet: wallet.publicKey,
        side: pos.winningSide!,
        usdcMint,
      });
      await signAndSend(tx);
      console.log(`Redeemed #${pos.marketId}: +$${pos.redeemableValueUsdc.toFixed(2)}`);
    } catch (err) {
      console.log(`Redeem #${pos.marketId}: ${err.message}`);
    }
    await new Promise(r => setTimeout(r, 300));
  }

  // Claim creator fees
  const myMarkets = await client.listMarkets({
    creator: wallet.publicKey.toBase58(),
  });

  for (const market of myMarkets) {
    const unclaimed = market.creatorFeesAccrued - market.creatorFeesClaimed;
    if (unclaimed <= 0) continue;

    try {
      const tx = await client.buildClaimFeesTx({
        marketId: market.id,
        wallet: wallet.publicKey,
        usdcMint,
      });
      await signAndSend(tx);
      console.log(`Claimed $${fromRawUsdc(unclaimed).toFixed(4)} fees from #${market.id}`);
    } catch {}
    await new Promise(r => setTimeout(r, 300));
  }
}

Part 5 — Main Loop

Tie everything together into a continuous loop:
const TARGET_ACTIVE_MARKETS = 7;

async function main() {
  console.log("PMX Trading Bot starting...");
  console.log(`Wallet: ${wallet.publicKey.toBase58()}`);

  // Run all loops concurrently
  await Promise.all([
    creatorLoop(),
    traderLoop(),
    resolverLoop(),
    collectorLoop(),
  ]);
}

async function creatorLoop() {
  while (true) {
    try {
      const active = await client.listMarkets({
        status: "active",
        creator: wallet.publicKey.toBase58(),
      });

      if (active.length < TARGET_ACTIVE_MARKETS) {
        console.log(`[CREATOR] ${active.length}/${TARGET_ACTIVE_MARKETS} active — creating new market`);
        await createMarket();
      } else {
        console.log(`[CREATOR] ${active.length} active — at target, skipping`);
      }
    } catch (err) {
      console.error("[CREATOR] Error:", err.message);
    }
    await new Promise(r => setTimeout(r, 5 * 60 * 1000));  // every 5 min
  }
}

async function traderLoop() {
  while (true) {
    try {
      await tradeMarkets();
    } catch (err) {
      console.error("[TRADER] Error:", err.message);
    }
    await new Promise(r => setTimeout(r, 75 * 1000));  // every 75 sec
  }
}

async function resolverLoop() {
  while (true) {
    try {
      await resolveExpiredMarkets();
    } catch (err) {
      console.error("[RESOLVER] Error:", err.message);
    }
    await new Promise(r => setTimeout(r, 2 * 60 * 1000));  // every 2 min
  }
}

async function collectorLoop() {
  while (true) {
    try {
      await collectWinnings();
    } catch (err) {
      console.error("[COLLECTOR] Error:", err.message);
    }
    await new Promise(r => setTimeout(r, 20 * 60 * 1000));  // every 20 min
  }
}

main().catch(console.error);

Running the Bot

Create a .env file:
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
AGENT_PRIVATE_KEY=<your-base58-private-key>
USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
COINMARKET_API_KEY=<your-cmc-api-key>
npx tsx bot.ts

Key Patterns to Remember

PatternWhy
Delay between transactionsSolana and RPC providers rate-limit. Add 300-500ms between calls
Quote before tradingAlways preview with quoteBuy to check price impact before executing
Check needsRedemptionOnly redeem positions where you hold winning tokens — skip worthless losers
getWalletPositions over getPositionsSingle RPC call vs N calls — dramatically faster
Historical price for resolutionUse CMC historical API for the exact resolution timestamp, not current price
Try/catch each transactionIndividual failures shouldn’t crash the loop
Check unclaimed before claimingSkip markets where creatorFeesAccrued - creatorFeesClaimed <= 0

Next Steps

SDK Reference

Full API reference for every method and type

Market Creation

Advanced market creation options