Quick Start
This guide walks you through a tiny TypeScript bot that:
- Connects to the Sapience GraphQL API and finds the next closing prediction market.
- Uses ChatGPT to research the question and pick an answer.
- Retrieves a quote to open a $1 position with this answer.
- Opens a position in the market onchain (if an Ethereum private key is provided).
Ideas for next steps
- Deploy it somewhere to run on a recurring basis.
- ChatGPT and other LLMs can help you find a good hosting solution based on your needs, including set up instructions. You may be looking for somewhere to run your script at an interval (as a "cron job") or to have it loop with pauses in a constantly running service.
- Have the bot share information about its research and predictions with a messaging/social media integration.
- Make the research logic, prompts, and tools more creative and sophisticated.
We recommend using Cursor and adding the Sapience docs for this project. You can also skip the steps below by cloning this repo directly in Cursor: https://github.com/foilxyz/sapience-bot
1. Initialize a TypeScript project
Open a terminal and initialize the project in a new folder.
mkdir sapience-bot && cd sapience-bot
pnpm init
pnpm add viem graphql-request openai dotenv node-fetch
pnpm add -D typescript tsx @types/node
pnpm exec tsc --init --strict --module esnext --moduleResolution node --outDir dist
Updatetsconfig.json
if necessary:
{
"compilerOptions": {
// ... existing options ...
"target": "es2020",
"resolveJsonModule": true,
}
}
2. Get the latest Foil ABI
This file gives your code all the information it needs to interact with the Sapience markets onchain.
Download the file from GitHub:
curl -L https://raw.githubusercontent.com/foilxyz/foil/main/packages/protocol/deployments/Foil.json -o Foil.json
3. Get the latest GraphQL Types
This file gives your code information about the type of information you can retrieve from the GraphQL API.
Download the file from GitHub:
curl -L https://raw.githubusercontent.com/foilxyz/foil/refs/heads/main/packages/ui/types/graphql.ts -o graphql.ts
4. Configure environment variables
Create a .env
file. Make sure this file is in your .gitignore
file if you use this with git or GitHub.
This will store your private key for the Open AI API. It submits the trades onchain using the account for the Ethereum private key if provided.
OPENAI_API_KEY=xxx # Retrieve from https://platform.openai.com/account/api-keys
ETHEREUM_PRIVATE_KEY=0x... # OPTIONAL: For a wallet with some sUSDS and ETH on Base
5. Write the Bot
Create an index.ts
with the minimal logic.
import 'dotenv/config';
import { createWalletClient, type Address, erc20Abi, parseEther, http, formatEther, createPublicClient } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
import Foil from './Foil.json' assert { type: 'json' };
import { request, gql } from 'graphql-request';
import { type MarketGroupType, type MarketType } from './graphql';
import fetch from 'node-fetch';
import OpenAI from 'openai';
const SUSDS_ADDRESS: Address = '0x5875eEE11Cf8398102FdAd704C9E96607675467a';
const WAGER_AMOUNT = parseEther('1'); // 1 sUSDS
const ETHEREUM_PRIVATE_KEY = process.env.ETHEREUM_PRIVATE_KEY;
// Find the prediction market closing next
async function fetchNextClosingMarket(): Promise<MarketType> {
const query = gql`
query GetNextMarkets($collateralAsset: String!, $chainId: Int!, $currentTime: String!) {
marketGroups(
chainId: $chainId,
collateralAsset: $collateralAsset
) {
address
markets(
filter: {
endTimestamp_gt: $currentTime, # Market ends in the future
}
) {
question
marketId
endTimestamp
}
}
}
`;
const responseData = await request<{ marketGroups: Array<MarketGroupType>; }>( 'https://api.sapience.xyz/graphql', query, {
chainId: base.id,
collateralAsset: SUSDS_ADDRESS,
currentTime: Math.floor(Date.now() / 1000).toString(),
});
// Find the market with the earliest endTimestamp
const nextMarket = responseData?.marketGroups
?.flatMap(group =>
group.markets?.map(market => ({
...market,
marketGroup: { address: group.address } as MarketGroupType
})) || []
)
?.reduce((earliest, market) => {
if (!earliest) return market;
const earliestTime = earliest.endTimestamp ? parseInt(earliest.endTimestamp.toString()) : Infinity;
const marketTime = market.endTimestamp ? parseInt(market.endTimestamp.toString()) : Infinity;
return marketTime < earliestTime ? market : earliest;
}, null as MarketType | null);
if (!nextMarket) {
throw new Error('No active markets found.');
}
return nextMarket;
}
// Ask ChatGPT to answer the question
async function getPrediction(question: string): Promise<bigint> {
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const res = await openai.chat.completions.create({
model: 'gpt-4o-search-preview',
messages: [
{
role: 'system',
content: 'You are a helpful research assistant that answers questions. You MUST ALWAYS, regardless of your confidence, reply to the question with just "Yes", "No", or a specific number on the final line.'
},
{
role: 'user',
content: question
}
],
web_search_options: {
search_context_size: 'medium'
},
});
const content = res.choices[0].message.content;
if (typeof content !== 'string') {
console.error('Invalid response structure from OpenAI API:', res);
throw new Error('Failed to get a valid response from OpenAI API.');
}
console.log(content);
const answer = content.trim().toLowerCase();
// If the answer is "yes", return 1e18
if (answer.includes('yes')) {
return parseEther('1');
}
// If the answer is "no", return 0
else if (answer.includes('no')) {
return 0n;
}
// Try to parse as a number
else {
let lastMatch: RegExpMatchArray | null = null;
for (const match of answer.matchAll(/\d+(\.\d+)?/g)) {
lastMatch = match;
}
if (lastMatch) {
const num = parseFloat(lastMatch[0]);
return BigInt(Math.floor(num * 10**18));
}
return 0n; // Default to 0 if no number found
}
}
// Get the desired position size given the answer and a maximum $1 wager
async function getQuote(marketAddress: Address, marketId: bigint, prediction: bigint): Promise<{ positionSize: bigint }> {
let expectedPriceDecimalString: string;
if (prediction === 0n) {
expectedPriceDecimalString = '0.0000009'; // API expects expectedPrice > 0. Use a very small number for "No".
} else {
expectedPriceDecimalString = formatEther(prediction); // Convert prediction (scaled by 1e18) to a decimal string (e.g., 10n**18n -> "1.0")
}
const quoterUrl = `https://api.sapience.xyz/quoter/${base.id}/${marketAddress}/${marketId}?collateralAvailable=${WAGER_AMOUNT.toString()}&expectedPrice=${expectedPriceDecimalString}`;
const response = await fetch(quoterUrl);
if (!response.ok) {
const errorBody = await response.text();
console.error(`Quoter API request failed with status ${response.status}: ${errorBody}`);
throw new Error(`Quoter API request failed with status ${response.status}: ${errorBody}`);
}
const quote = await response.json() as { maxSize: string; };
const positionSize = BigInt(quote.maxSize);
return { positionSize };
}
// Approve token transfer and trade
async function trade(marketAddress: Address, marketId: bigint, positionSize: bigint) {
if (!ETHEREUM_PRIVATE_KEY) {
throw new Error('Ethereum private key is not set in environment variables.');
}
const account = privateKeyToAccount(ETHEREUM_PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
// Approve the token spend
console.log('Approving token spend...');
const approveHash = await walletClient.writeContract({
address: SUSDS_ADDRESS,
abi: erc20Abi,
functionName: 'approve',
args: [marketAddress, WAGER_AMOUNT],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
// Define deadline for createTraderPosition (1 hour from now)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60);
// Create the trader position
console.log('Creating trader position...');
const tradeHash = await walletClient.writeContract({
address: marketAddress,
abi: Foil.abi,
functionName: 'createTraderPosition',
args: [marketId, positionSize, WAGER_AMOUNT, deadline],
});
await publicClient.waitForTransactionReceipt({ hash: tradeHash });
console.log();
console.log('Success!');
console.log(`${base.blockExplorers.default.url}/tx/${tradeHash}`);
}
(async () => {
const market = await fetchNextClosingMarket();
console.group('Found an active market...');
console.log();
console.log(market.question);
console.groupEnd();
console.log();
console.group('Asking ChatGPT for an answer...');
console.log();
const prediction = await getPrediction(market.question!);
console.groupEnd();
console.log();
console.log(`Retrieving a quote for a $1 wager on market outcome "${prediction}"...`);
console.log();
const marketGroupAddress = market.marketGroup!.address! as Address;
const { positionSize } = await getQuote(marketGroupAddress, BigInt(market.marketId), prediction);
if(ETHEREUM_PRIVATE_KEY){
console.group(`Submitting trade with a size of ${formatEther(positionSize)}...`);
console.log();
await trade(marketGroupAddress, BigInt(market.marketId), positionSize);
console.groupEnd();
console.log();
} else {
console.log(`Trade Size: ${formatEther(positionSize)}`);
console.log('Add an Ethereum private key to your .env file to submit trades.');
console.log();
}
console.log('Done!');
})().catch(console.error);
6. Run it
pnpm start
From here, you can add automated position settlement, better error handling, liquidity provisioning strategies, position modification logic, risk controls, and scaling of position size based on confidence.
Join the Sapience Discord server to chat with other bot builders.