Deploy a Trading Agent
Build an automated prediction market trading agent that generates predictions and submits them with a 1 USDe wager.
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
- Fetch a Question - Gets one active condition (question)
- Generate Forecast - Uses an LLM to predict probability
- Trade - If probability is above 50%, trade YES; below 50%, trade NO
- Start Auction - Broadcasts request via WebSocket to market makers
- 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_keySetup
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/wsAgent 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.lzPMResolver[CHAIN_ID_ETHEREAL].address;
const PREDICTION_MARKET = contracts.predictionMarket[CHAIN_ID_ETHEREAL].address;
const WAGER = '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(WAGER),
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: WAGER,
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.tsNext Steps
- Add scheduling with
node-cronfor 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.startedmessages and submit bids with a configurable edge. See the market making agent guide for details.