Skip to content

Prediction Market Trading Bots

This guide builds on the Forecasting Agent guide. Here, we'll add a minimal $1 trading flow using the Quoter API and execute trades in an agent "action".

Add a Dollar Wager

Add a bot action that sizes a $1 wager with the Quoter API, then submits the trade. Use this as a drop‑in action in your agent.

Action (e.g. src/actions/dollarWager.ts):

import Foil from './Foil.json' assert { type: 'json' };
import { createWalletClient, createPublicClient, http, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
 
const WAGER_AMOUNT = parseEther('1'); // 1 sUSDS
 
export function forecastToExpectedPriceDecimalString(percent: number): string {
  const clamped = Math.max(0, Math.min(100, percent));
  if (clamped === 0) return '0.0000009';
  return (clamped / 100).toString();
}
 
export async function getQuote({ chainId, marketGroupAddress, marketId, forecastPercent }: {
  chainId: number;
  marketGroupAddress: string;
  marketId: number | string;
  forecastPercent: number;
}) {
  const expectedPrice = forecastToExpectedPriceDecimalString(forecastPercent);
  const url = `https://api.sapience.xyz/quoter/${chainId}/${marketGroupAddress}/${marketId}?collateralAvailable=${WAGER_AMOUNT.toString()}&expectedPrice=${expectedPrice}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Quoter error ${res.status}`);
  const { maxSize } = await res.json() as { maxSize: string };
  return { positionSize: BigInt(maxSize) };
}
 
async function tradeAction({ marketAddress, marketId, positionSize }: {
  marketAddress: `0x${string}`;
  marketId: bigint;
  positionSize: bigint; // from Quoter
}) {
  const ETHEREUM_PRIVATE_KEY = process.env.ETHEREUM_PRIVATE_KEY as `0x${string}` | undefined;
  if (!ETHEREUM_PRIVATE_KEY) throw new Error('Missing ETHEREUM_PRIVATE_KEY');
 
  const account = privateKeyToAccount(ETHEREUM_PRIVATE_KEY);
  const walletClient = createWalletClient({ account, chain: base, transport: http() });
  const publicClient = createPublicClient({ chain: base, transport: http() });
 
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60);
  const hash = await walletClient.writeContract({
    address: marketAddress,
    abi: Foil.abi,
    functionName: 'createTraderPosition',
    args: [marketId, positionSize, WAGER_AMOUNT, deadline],
  });
  await publicClient.waitForTransactionReceipt({ hash });
}
 
export async function dollarWagerAction({ chainId, marketGroupAddress, marketId, forecastPercent }: {
  chainId: number;
  marketGroupAddress: string;
  marketId: number | string;
  forecastPercent: number;
}) {
  const { positionSize } = await getQuote({ chainId, marketGroupAddress, marketId, forecastPercent });
  await tradeAction({
    marketAddress: marketGroupAddress as `0x${string}`,
    marketId: BigInt(marketId),
    positionSize,
  });
}

Wire it into the bot (e.g. src/index.ts in your Sage/Eliza template):

import { base } from 'viem/chains';
import { dollarWagerAction } from './actions/dollarWager';
 
// Inside your forecast loop or action registry
async function onForecast({ marketGroupAddress, marketId, forecastPercent }: {
  marketGroupAddress: string;
  marketId: number | string;
  forecastPercent: number;
}) {
  // Apply risk checks (budget caps, min confidence) before trading
  await dollarWagerAction({
    chainId: base.id,
    marketGroupAddress,
    marketId,
    forecastPercent,
  });
}

Limit Order

Alternatively, implement a client‑side limit order that waits for price to reach your level before quoting and submitting.

Action (e.g. src/actions/limitOrder.ts):

import { base } from 'viem/chains';
import { dollarWagerAction, getQuote } from './dollarWager';
 
async function getCurrentYesPrice({ chainId, marketGroupAddress, marketId }: {
  chainId: number;
  marketGroupAddress: string;
  marketId: number | string;
}): Promise<number> {
  // Implement with your preferred price source (polling/subscription). Return 0..1
  throw new Error('getCurrentYesPrice not implemented');
}
 
export async function limitOrderAction({
  chainId,
  marketGroupAddress,
  marketId,
  limitPercent,
}: {
  chainId: number;
  marketGroupAddress: string;
  marketId: number | string;
  limitPercent: number; // e.g. 62 means buy YES at <= 0.62
}) {
  const limit = limitPercent / 100;
  while (true) {
    const current = await getCurrentYesPrice({ chainId, marketGroupAddress, marketId });
    if (current <= limit) {
      const { positionSize } = await getQuote({
        chainId,
        marketGroupAddress,
        marketId,
        forecastPercent: limitPercent,
      });
      await dollarWagerAction({ chainId, marketGroupAddress, marketId, forecastPercent: limitPercent });
      return;
    }
    await new Promise((r) => setTimeout(r, 5_000));
  }
}

Wire it into the bot (e.g. src/index.ts):

import { base } from 'viem/chains';
import { limitOrderAction } from './actions/limitOrder';
 
async function onForecastWithLimit({ marketGroupAddress, marketId }: {
  marketGroupAddress: string;
  marketId: number | string;
}) {
  // Example: buy YES only at <= 0.62
  await limitOrderAction({
    chainId: base.id,
    marketGroupAddress,
    marketId,
    limitPercent: 62,
  });
}