Skip to main content

circuit.toml

name = "my-polymarket-agent"
tagline = "Trades on Polymarket markets"
walletType = "ethereum"
allowedExecutionModes = ["auto"]
runtimeIntervalMinutes = 60
version = "0.0.1"

[startingAsset]
network = "ethereum:137"
address = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"
minimumAmount = "5000000" # 5 USDC (6 decimals)

Example

import { Agent, type AgentContext, type CurrentPosition } from "@circuitorg/agent-sdk";

const BUY_AMOUNT = 5; // USD to spend per cycle

// Polygon USDC address
const USDC = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174";

const CLOB_API = "https://clob.polymarket.com";

async function checkOrderBookLiquidity(tokenId: string, side: string, minUsd: number): Promise<boolean> {
  /** Check that the CLOB order book has enough liquidity for our order. */
  try {
    const response = await fetch(`${CLOB_API}/book?token_id=${tokenId}`, { signal: AbortSignal.timeout(10000) });
    if (!response.ok) return false;

    const book = await response.json();
    const orders = book[side === "BUY" ? "asks" : "bids"] || [];

    let totalUsd = 0;
    for (const order of orders) {
      totalUsd += parseFloat(order.price) * parseFloat(order.size);
    }

    return totalUsd >= minUsd;
  } catch {
    return false;
  }
}

interface MarketInfo {
  tokenId: string;
  question: string;
  outcome: string;
  price: string;
}

async function findLiquidSportsMarket(minLiquidityUsd: number): Promise<MarketInfo | null> {
  /** Find the most liquid active sports market on Polymarket via the Gamma API. */
  try {
    const params = new URLSearchParams({
      tag_slug: "sports",
      limit: "10",
      active: "true",
      closed: "false",
      archived: "false",
      order: "volume24hr",
      ascending: "false",
    });

    const response = await fetch(
      `https://gamma-api.polymarket.com/events/pagination?${params}`,
      { signal: AbortSignal.timeout(10000) }
    );
    const { data: events = [] } = await response.json();

    for (const event of events) {
      for (const market of event.markets || []) {
        if (!market.acceptingOrders) continue;

        const clobTokenIds: string[] = JSON.parse(market.clobTokenIds || "[]");
        const outcomes: string[] = JSON.parse(market.outcomes || "[]");
        const outcomePrices: string[] = JSON.parse(market.outcomePrices || "[]");

        if (!clobTokenIds.length || !outcomes.length) continue;

        const price = outcomePrices.length ? parseFloat(outcomePrices[0]) : 0;

        // Skip markets with extreme prices (thin liquidity on one side)
        if (price < 0.10 || price > 0.90) continue;

        const tokenId = clobTokenIds[0];

        // Verify order book has enough ask liquidity for our buy
        if (!(await checkOrderBookLiquidity(tokenId, "BUY", minLiquidityUsd))) continue;

        return {
          tokenId,
          question: market.question || "Unknown",
          outcome: outcomes[0],
          price: outcomePrices.length ? outcomePrices[0] : "?",
        };
      }
    }
  } catch (e) {
    console.error(`Failed to fetch markets: ${e}`);
  }

  return null;
}

async function run(agent: AgentContext): Promise<void> {
  // 1. Redeem any settled positions
  const redemption = await agent.platforms.polymarket.redeemPositions();
  if (redemption.success && redemption.data) {
    const redeemed = redemption.data.filter((r) => r.success);
    if (redeemed.length > 0) {
      await agent.log(`Redeemed ${redeemed.length} settled positions`);
      for (const r of redeemed) {
        if (r.position) {
          await agent.log(`  ${r.position.question} (${r.position.outcome}): $${r.position.valueUsd}`);
        }
      }
    }
  }

  // 2. Check current positions and log Polymarket portfolio
  const result = await agent.getCurrentPositions();
  if (!result.success || !result.data) {
    await agent.log(result.error || "Failed to get positions", { error: true });
    return;
  }

  for (const pos of result.data.positions) {
    if (pos.polymarketMetadata) {
      const pm = pos.polymarketMetadata;
      await agent.log(`${pm.question} [${pm.outcome}]: $${pm.valueUsd} (PnL: $${pm.pnlUsd})`);
    }
  }

  // 3. Find USDC balance
  const usdc = result.data.positions.find(
    (p) => p.assetAddress.toLowerCase() === USDC
  );
  const usdcBalance = usdc ? parseFloat(usdc.currentQty) / 1e6 : 0;
  await agent.log(`USDC balance: $${usdcBalance.toFixed(2)}`);

  if (usdcBalance < BUY_AMOUNT) {
    await agent.log("Not enough USDC to buy");
    return;
  }

  // 4. Find the most liquid sports market (with order book liquidity check)
  const market = await findLiquidSportsMarket(BUY_AMOUNT);
  if (!market) {
    await agent.log("No active sports markets with sufficient liquidity found");
    return;
  }

  await agent.log(`Target market: ${market.question} [${market.outcome}] @ ${market.price}`);

  // 5. Buy a position
  const buy = await agent.platforms.polymarket.marketOrder({
    tokenId: market.tokenId,
    size: BUY_AMOUNT,
    side: "BUY",
  });

  if (buy.success && buy.data) {
    const info = buy.data.orderInfo;
    if (info.totalPriceUsd) {
      await agent.log(`Bought shares: $${info.totalPriceUsd} at $${info.priceUsd}/share (order ${info.orderId})`);
    } else {
      await agent.log(`Order placed: ${info.orderId} (${info.size} shares, ${info.side})`);
    }
  } else {
    await agent.log(`Buy failed: ${buy.error}`, { error: true });
  }
}

async function unwind(agent: AgentContext, positions: CurrentPosition[]): Promise<void> {
  // Sell all Polymarket positions
  const result = await agent.getCurrentPositions();
  if (!result.success || !result.data) {
    await agent.log("Failed to get positions for unwind", { error: true });
    return;
  }

  let sold = 0;
  for (const pos of result.data.positions) {
    if (!pos.polymarketMetadata) continue;

    const tokenId = pos.tokenId;
    if (!tokenId) continue;

    const shares = parseFloat(pos.currentQty) / 1e6; // Convert from raw units
    if (shares <= 0) continue;

    const sell = await agent.platforms.polymarket.marketOrder({
      tokenId,
      size: shares,
      side: "SELL",
    });

    if (sell.success && sell.data) {
      const pm = pos.polymarketMetadata;
      await agent.log(`Sold ${pm.question} (${pm.outcome}): $${sell.data.orderInfo.totalPriceUsd}`);
      sold++;
    } else {
      await agent.log(`Failed to sell position: ${sell.error}`, { error: true });
    }
  }

  if (sold === 0) {
    await agent.log("No Polymarket positions to sell");
  }

  // Redeem any settled positions
  await agent.platforms.polymarket.redeemPositions();
}

const agent = new Agent({
  runFunction: run,
  unwindFunction: unwind,
});

export default agent.getExport();

Sample Output

Agent run started
Redeemed 1 settled positions
  Will Spain win the 2026 FIFA World Cup? (Yes): $5.12
USDC balance: $12.34
Target market: Will Argentina win Copa America 2025? [Yes] @ 0.62
Bought shares: $5.00 at $0.62/share (order 8837291045)
Agent run completed

How It Works

  1. Redeem settled positions: Claims winnings from any resolved markets
  2. Log portfolio: Shows current Polymarket positions with PnL
  3. Check balance: Finds USDC balance from current positions
  4. Find market: Queries the Gamma API for liquid sports markets, filters by price range (0.10-0.90), and verifies order book depth via the CLOB API
  5. Buy shares: Places a market buy order on the best available market
  6. Unwind: Sells all open Polymarket positions and redeems settled ones

Notes

  • Always check order book liquidity before calling marketOrder. The CLOB will error if there aren’t enough asks/bids to fill your order. See Troubleshooting for details.
  • Polymarket uses USDC on Polygon (ethereum:137). Set your startingAsset accordingly.
  • size means different things for buys vs sells: USD amount for BUY, share count for SELL.
  • Token IDs are long numeric strings from Polymarket’s API — they identify a specific outcome (e.g., “Yes” or “No”) in a market.
  • Use redeemPositions() to claim winnings after markets resolve. It’s good practice to call it at the start of each run cycle.