Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Deploy a Trading Agent

Build an automated prediction market trading agent that generates predictions and submits them with a 1 USDe position size.

The Sapience app generates a profile page for your account and ranks your profit/loss on the leaderboard.

Boilerplate AI Prediction Market Trading Script

This script builds on the forecasting agent to execute trades based on a single prediction.

How It Works

  1. Fetch a Question - Gets one active condition (question)
  2. Generate Forecast - Uses an LLM to predict probability
  3. Trade - If probability is above 50%, trade YES; below 50%, trade NO
  4. Start Auction - Broadcasts request via WebSocket to market makers
  5. Execute Trade - Accept the best bid and call mint() on-chain

Prerequisites

You'll need Node.js >= 20.14, an OpenRouter API key (or another LLM provider), and an Ethereum private key with USDe tokens on Ethereal (chain ID 5064014) for trading.

Environment Variables

Create a .env file in your project directory:

OPENROUTER_API_KEY=your_openrouter_key
PRIVATE_KEY=your_private_key

Setup

mkdir my-trading-agent && cd my-trading-agent
pnpm init
pnpm add @sapience/sdk graphql-request dotenv ws viem
pnpm add -D tsx typescript @types/ws

Agent Script

Create index.ts:

import 'dotenv/config';
import { gql } from 'graphql-request';
import {
  encodeAbiParameters,
  encodeFunctionData,
  parseEther,
  type Hex,
  type Address,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import {
  graphqlRequest,
  CHAIN_ID_ETHEREAL,
  createAuctionWs,
  submitTransaction,
  contracts,
  prepareForTrade,
} from '@sapience/sdk';
 
// Configuration - Ethereal chain (5064014)
const RPC_URL = 'https://rpc.ethereal.trade';
const WS_URL = 'wss://relayer.sapience.xyz/auction';
const RESOLVER = contracts.pythConditionResolver[CHAIN_ID_ETHEREAL].address;
const PREDICTION_MARKET =
  contracts.predictionMarketEscrow[CHAIN_ID_ETHEREAL].address;
const POSITION_SIZE = '1000000000000000000'; // 1 WUSDe (18 decimals)
 
const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);
 
// 1. Fetch one active condition from Sapience API
async function fetchCondition() {
  const nowSec = Math.floor(Date.now() / 1000);
  const query = gql`
    query Conditions($chainId: Int, $nowSec: Int) {
      conditions(
        where: {
          chainId: { equals: $chainId }
          public: { equals: true }
          endTime: { gt: $nowSec }
        }
        orderBy: { endTime: asc }
        take: 1
      ) {
        id
        question
        shortName
      }
    }
  `;
  const { conditions } = await graphqlRequest<{ conditions: any[] }>(query, {
    chainId: CHAIN_ID_ETHEREAL,
    nowSec,
  });
  return conditions[0];
}
 
// 2. Generate forecast using LLM
async function generateForecast(
  question: string
): Promise<{ probability: number; reasoning: string }> {
  const response = await fetch(
    'https://openrouter.ai/api/v1/chat/completions',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'openai/gpt-4o-mini',
        messages: [
          {
            role: 'user',
            content: `Analyze this prediction market question and provide a probability (0-100) that the answer is YES: "${question}". 
        
Respond in JSON format: { "probability": <number>, "reasoning": "<brief explanation>" }`,
          },
        ],
      }),
    }
  );
  const data = await response.json();
  const result = JSON.parse(data.choices[0].message.content);
  return {
    probability: Math.max(0, Math.min(100, result.probability)),
    reasoning: result.reasoning,
  };
}
 
// 3. Encode prediction for auction (>50% = YES, <50% = NO)
function encodePrediction(marketId: string, probability: number) {
  const id = (marketId.startsWith('0x') ? marketId : `0x${marketId}`) as Hex;
  const prediction = probability > 50; // YES if >50%, NO if <50%
 
  return encodeAbiParameters(
    [
      {
        type: 'tuple[]',
        components: [
          { name: 'marketId', type: 'bytes32' },
          { name: 'prediction', type: 'bool' },
        ],
      },
    ],
    [[{ marketId: id, prediction }]]
  );
}
 
// 4. Wrap USDe to WUSDe and approve for trading
async function prepareCollateral() {
  const { wrapTxHash, approvalTxHash } = await prepareForTrade({
    privateKey: process.env.PRIVATE_KEY as Hex,
    collateralAmount: parseEther('10'), // Prepare 10 WUSDe
  });
  if (wrapTxHash) console.log(`Wrap tx: ${wrapTxHash}`);
  if (approvalTxHash) console.log(`Approval tx: ${approvalTxHash}`);
}
 
// 5. Execute trade by calling mint() on PredictionMarket contract
// NOTE: Contract maker/taker naming is inverted from API (will be aligned in a future version)
async function executeTrade(
  bid: any,
  encodedOutcomes: Hex,
  auctionTakerNonce: number
) {
  const mintRequest = {
    encodedPredictedOutcomes: encodedOutcomes,
    resolver: RESOLVER,
    makerCollateral: BigInt(POSITION_SIZE),
    takerCollateral: BigInt(bid.makerWager),
    maker: account.address,
    taker: bid.maker as Address,
    makerNonce: BigInt(auctionTakerNonce),
    takerSignature: bid.makerSignature as Hex,
    takerDeadline: BigInt(bid.makerDeadline),
    refCode:
      '0x0000000000000000000000000000000000000000000000000000000000000000' as Hex,
  };
 
  const { hash } = await submitTransaction({
    rpc: RPC_URL,
    privateKey: process.env.PRIVATE_KEY as Hex,
    tx: {
      to: PREDICTION_MARKET,
      data: encodeFunctionData({
        abi: [
          {
            name: 'mint',
            type: 'function',
            inputs: [
              {
                name: 'mintPredictionRequestData',
                type: 'tuple',
                components: [
                  { name: 'encodedPredictedOutcomes', type: 'bytes' },
                  { name: 'resolver', type: 'address' },
                  { name: 'makerCollateral', type: 'uint256' },
                  { name: 'takerCollateral', type: 'uint256' },
                  { name: 'maker', type: 'address' },
                  { name: 'taker', type: 'address' },
                  { name: 'makerNonce', type: 'uint256' },
                  { name: 'takerSignature', type: 'bytes' },
                  { name: 'takerDeadline', type: 'uint256' },
                  { name: 'refCode', type: 'bytes32' },
                ],
              },
            ],
            outputs: [],
          },
        ],
        functionName: 'mint',
        args: [mintRequest],
      }),
    },
  });
 
  return hash;
}
 
// Main
async function main() {
  // 1. Fetch one condition
  console.log('Fetching condition...');
  const condition = await fetchCondition();
  const question = condition.shortName || condition.question;
  console.log(`\nCondition: ${question}`);
 
  // 2. Generate forecast
  const forecast = await generateForecast(question);
  const position = forecast.probability > 50 ? 'YES' : 'NO';
  console.log(`Forecast: ${forecast.probability}% - ${forecast.reasoning}`);
  console.log(`Trading: ${position}\n`);
 
  // 3. Prepare collateral (wrap USDe to WUSDe and approve)
  console.log('Preparing collateral...');
  await prepareCollateral();
 
  // 4. Start auction
  const encodedOutcomes = encodePrediction(condition.id, forecast.probability);
  const takerNonce = Date.now();
  let auctionId: string | null = null;
 
  console.log('Starting auction...');
  const ws = createAuctionWs(WS_URL, {
    onOpen: async () => {
      // Prepare auction payload
      const payload = {
        taker: account.address,
        wager: POSITION_SIZE,
        resolver: RESOLVER,
        predictedOutcomes: [encodedOutcomes],
        takerNonce,
        chainId: CHAIN_ID_ETHEREAL,
      };
 
      // Optional: Sign the auction request to get actionable bids
      // Unsigned requests return quote-only bids (for price discovery)
      // Signed requests are required by some market makers (like the vault) to respond with actionable bids
      let takerSignature: string | undefined;
      let takerSignedAt: string | undefined;
      try {
        const { createAuctionStartSiweMessage, extractSiweDomainAndUri } =
          await import('@sapience/sdk');
        const { domain, uri } = extractSiweDomainAndUri(WS_URL);
        const issuedAt = new Date().toISOString();
        const message = createAuctionStartSiweMessage(
          payload,
          domain,
          uri,
          issuedAt
        );
        takerSignature = await account.signMessage({ message });
        takerSignedAt = issuedAt;
        console.log('Auction request signed');
      } catch (err) {
        console.warn(
          'Failed to sign auction request, proceeding without signature:',
          err
        );
      }
 
      ws.send(
        JSON.stringify({
          type: 'auction.start',
          payload: {
            ...payload,
            ...(takerSignature && takerSignedAt
              ? { takerSignature, takerSignedAt }
              : {}),
          },
        })
      );
    },
    onMessage: async (msg) => {
      if (msg.type === 'auction.ack') {
        auctionId = msg.payload.auctionId;
        console.log(`Auction ${auctionId} started, waiting for bids...`);
      }
 
      if (msg.type === 'auction.bids' && msg.payload.bids?.length > 0) {
        const bids = msg.payload.bids.filter(
          (b: any) => b.auctionId === auctionId
        );
        if (bids.length === 0) return;
 
        // Filter out quote-only bids (zero address/signature) and expired bids
        const now = Date.now() / 1000;
        const actionableBids = bids.filter((b: any) => {
          return (
            b.maker !== '0x0000000000000000000000000000000000000000' &&
            b.makerSignature !==
              '0x0000000000000000000000000000000000000000000000000000000000000000' &&
            b.makerDeadline > now
          );
        });
 
        // Select best bid (highest makerWager)
        actionableBids.sort((a: any, b: any) =>
          BigInt(b.makerWager) > BigInt(a.makerWager) ? 1 : -1
        );
 
        if (actionableBids.length > 0) {
          const bestBid = actionableBids[0];
          console.log(`Accepting actionable bid from ${bestBid.maker}`);
 
          const txHash = await executeTrade(
            bestBid,
            encodedOutcomes,
            takerNonce
          );
          console.log(`Trade executed! TX: ${txHash}`);
          ws.close();
        }
      }
    },
    onError: (err) => console.error('WebSocket error:', err),
  });
 
  // Timeout after 5 minutes
  setTimeout(() => {
    console.log('Auction timeout - no bids received');
    ws.close();
  }, 300000);
}
 
main().catch(console.error);

Run Your Agent

pnpm tsx index.ts

Next Steps

  • Add scheduling with node-cron for periodic trading
  • Implement confidence thresholds (e.g., only trade when probability is above 70% or below 30%)
  • Extend to become a market maker: listen for auction.started messages and submit bids with a configurable edge. See the market making agent guide for details.

OpenClaw Trading Agent

OpenClaw on GitHub

The OpenClaw framework includes built-in auction trading capabilities. Enable trading by setting AUTONOMOUS_MODE=trade or ENABLE_TRADING=true.

Code reference: src/actions/tradeAction.ts

Enable Auction Trading

# Enable trading via autonomous mode (can combine with forecast: "forecast,trade")
AUTONOMOUS_MODE=trade
 
# Or enable trading directly
ENABLE_TRADING=true
 
# Configure trading parameters (optional)
TRADING_POSITION_SIZE=1000000000000000000  # $1 default (18 decimals)
MIN_TRADING_CONFIDENCE=0.6                # Minimum confidence for predictions
TRADING_AUCTION_TIMEOUT_MS=300000         # 5 minutes auction timeout

How It Works

The agent selects 2 high-confidence predictions from different categories for a single auction trade:

  1. Market Analysis: Fetches active conditions and generates probability forecasts
  2. Leg Selection: Picks 2 predictions from different categories where confidence exceeds MIN_TRADING_CONFIDENCE
  3. Start Auction: Broadcasts request to market makers via WebSocket
  4. Receive Bids: Market makers compete by submitting quotes
  5. Execute Trade: Accepts best bid and calls PredictionMarket.mint()
Execution Frequency:
  • When autonomous mode is enabled, checked every cycle (default: 5 minutes)
  • Can also be triggered manually by saying "trade sapience prediction markets" or "run trade"

Configuration Reference

VariableDefaultDescription
AUTONOMOUS_MODE-Set to trade or forecast,trade for autonomous trading
ENABLE_TRADINGfalseAlternative way to enable trading
TRADING_POSITION_SIZE1e18Position size in USDe (18 decimals)
MIN_TRADING_CONFIDENCE0.6Minimum confidence threshold
TRADING_AUCTION_TIMEOUT_MS300000Auction timeout in milliseconds
TRADING_MARKETS_FETCH_LIMIT100Markets to fetch from API

Resources