- Create crypto price markets on a schedule
- Monitor prices and place trades when it detects an edge
- Resolve expired markets using actual price data
- Redeem winnings and claim creator fees automatically
Prerequisites
npm install @pmx/parimutuel-sdk @solana/web3.js bs58
Setup
import { Connection, PublicKey, Keypair, Transaction } from "@solana/web3.js";
import bs58 from "bs58";
import {
ParimutuelClient,
PMX_PROGRAM_ID,
toRawUsdc,
fromRawUsdc,
getImpliedOdds,
} from "@pmx/parimutuel-sdk";
const connection = new Connection(process.env.SOLANA_RPC_URL!, "confirmed");
const client = new ParimutuelClient(connection, new PublicKey(PMX_PROGRAM_ID));
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.AGENT_PRIVATE_KEY!));
const usdcMint = new PublicKey(process.env.USDC_MINT!);
async function signAndSend(tx: Transaction): Promise<string> {
tx.sign(wallet);
const sig = await connection.sendRawTransaction(tx.serialize());
await connection.confirmTransaction(sig, "confirmed");
return sig;
}
Part 1 — Market Creator
The creator module spins up new markets on a schedule:const COINS = [
{ symbol: "BTC", cmcId: 1, name: "Bitcoin" },
{ symbol: "ETH", cmcId: 1027, name: "Ethereum" },
{ symbol: "SOL", cmcId: 5426, name: "Solana" },
];
async function fetchPrice(symbol: string): Promise<number> {
const res = await fetch(
`https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?symbol=${symbol}`,
{ headers: { "X-CMC_PRO_API_KEY": process.env.COINMARKET_API_KEY! } }
);
const data = await res.json();
return data.data[symbol].quote.USD.price;
}
async function createMarket() {
// Pick a random coin
const coin = COINS[Math.floor(Math.random() * COINS.length)];
const price = await fetchPrice(coin.symbol);
// Set threshold near current price (round to significant digits)
const precision = price > 1000 ? 100 : price > 100 ? 10 : price > 1 ? 0.1 : 0.001;
const threshold = Math.round(price / precision) * precision;
// Market resolves in 30 minutes
const durationMin = 15 + Math.floor(Math.random() * 30); // 15-45 min
const resolutionTime = Math.floor(Date.now() / 1000) + durationMin * 60;
const resDate = new Date(resolutionTime * 1000);
const timeStr = resDate.toLocaleTimeString("en-US", {
hour: "numeric", minute: "2-digit", timeZone: "UTC",
}) + " UTC";
const ticker = coin.symbol.toUpperCase();
const seedAmount = toRawUsdc(10 + Math.floor(Math.random() * 15)); // $10-$25
console.log(`Creating: "${ticker} above $${threshold} by ${timeStr}" — seed: $${fromRawUsdc(seedAmount)}`);
const { createTx, initMintsTx, setMetadataTx, marketId } = await client.createMarketFull({
wallet: wallet.publicKey,
title: `${ticker} above $${threshold.toLocaleString()} by ${timeStr}`,
question: `Will ${coin.name} (${ticker}) be above $${threshold.toLocaleString()} by ${timeStr}?`,
yesTicker: `${ticker}UP`,
noTicker: `${ticker}DN`,
resolutionTime,
seedAmount,
tradeFeeBps: 200,
usdcMint,
imageUrl: `https://s2.coinmarketcap.com/static/img/coins/128x128/${coin.cmcId}.png`,
});
await signAndSend(createTx);
await signAndSend(initMintsTx);
if (setMetadataTx) await signAndSend(setMetadataTx);
console.log(`✓ Created market #${marketId}`);
return marketId;
}
Part 2 — Trader
The trader monitors active markets, compares market odds to actual price movements, and trades when it sees an edge:async function tradeMarkets() {
const markets = await client.listMarkets({ status: "active" });
for (const market of markets) {
// Parse the ticker symbol from the title (e.g. "BTC above $95,000...")
const match = market.title.match(/^(\w+)\s+above\s+\$([0-9,.]+)/i);
if (!match) continue;
const symbol = match[1];
const threshold = parseFloat(match[2].replace(/,/g, ""));
// Get current price
let currentPrice: number;
try {
currentPrice = await fetchPrice(symbol);
} catch {
continue;
}
// Calculate expected probability based on price distance
const pctFromThreshold = (currentPrice - threshold) / threshold;
const timeLeft = market.resolutionTime - Math.floor(Date.now() / 1000);
const timeWeight = Math.max(0.1, Math.min(1, timeLeft / 1800));
// Simple model: if price is above threshold, YES is more likely
let expectedYes: number;
if (pctFromThreshold > 0.02) expectedYes = 0.7 + Math.min(0.25, pctFromThreshold * 5);
else if (pctFromThreshold < -0.02) expectedYes = 0.3 - Math.min(0.25, Math.abs(pctFromThreshold) * 5);
else expectedYes = 0.5 + pctFromThreshold * 10;
// As time runs out, become more certain
if (timeLeft < 300) {
expectedYes = currentPrice > threshold ? 0.95 : 0.05;
}
const marketYes = market.odds.yes;
const edge = expectedYes - marketYes;
if (Math.abs(edge) < 0.05) continue; // need at least 5% edge
const side = edge > 0 ? "YES" : "NO";
const amount = toRawUsdc(Math.min(5, 1 + Math.abs(edge) * 10));
const quote = client.quoteBuy(
market.yesReserve, market.noReserve,
amount, side, market.tradeFeeBps, market.seedLiquidity,
);
if (quote.priceImpactBps > 500) continue; // skip if too much slippage
try {
const { tx } = await client.buildBuyTx({
marketId: market.id,
wallet: wallet.publicKey,
side, amount, usdcMint,
});
await signAndSend(tx);
console.log(`Traded $${fromRawUsdc(amount)} ${side} on #${market.id} "${market.title}" (edge: ${(edge * 100).toFixed(1)}%, price: $${currentPrice.toFixed(2)})`);
} catch (err) {
console.log(`Trade failed on #${market.id}: ${err.message}`);
}
await new Promise(r => setTimeout(r, 300));
}
}
Part 3 — Resolver
The resolver checks for expired markets and determines the winner using historical price data:async function resolveExpiredMarkets() {
const expired = await client.getExpiredUnresolved();
for (const market of expired) {
// Only resolve markets I created
if (market.creator !== wallet.publicKey.toBase58()) continue;
const match = market.title.match(/^(\w+)\s+above\s+\$([0-9,.]+)/i);
if (!match) {
console.log(`Can't parse market #${market.id} title — skipping`);
continue;
}
const symbol = match[1];
const threshold = parseFloat(match[2].replace(/,/g, ""));
// Get the price at the exact resolution time using CoinMarketCap historical API
let priceAtResolution: number;
try {
const isoTime = new Date(market.resolutionTime * 1000).toISOString();
const res = await fetch(
`https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/historical?symbol=${symbol}&time_start=${isoTime}&count=1`,
{ headers: { "X-CMC_PRO_API_KEY": process.env.COINMARKET_API_KEY! } }
);
const data = await res.json();
const quotes = data.data[symbol]?.[0]?.quotes;
priceAtResolution = quotes?.[0]?.quote?.USD?.price;
} catch {
// Fallback to current price if historical isn't available
priceAtResolution = await fetchPrice(symbol);
}
const winningSide = priceAtResolution > threshold ? "YES" : "NO";
console.log(`Resolving #${market.id}: ${symbol} was $${priceAtResolution.toFixed(2)} (threshold: $${threshold}) → ${winningSide}`);
try {
const tx = await client.buildResolveTx({
marketId: market.id,
wallet: wallet.publicKey,
winningSide,
usdcMint,
});
await signAndSend(tx);
console.log(`✓ Resolved market #${market.id} → ${winningSide}`);
} catch (err) {
console.log(`✗ Resolution failed: ${err.message}`);
}
await new Promise(r => setTimeout(r, 500));
}
}
Part 4 — Redeemer & Fee Claimer
Collect all value from resolved markets:async function collectWinnings() {
// Redeem winning positions
const positions = await client.getWalletPositions(wallet.publicKey);
const redeemable = positions.filter(p => p.needsRedemption);
for (const pos of redeemable) {
try {
const tx = await client.buildRedeemTx({
marketId: pos.marketId,
wallet: wallet.publicKey,
side: pos.winningSide!,
usdcMint,
});
await signAndSend(tx);
console.log(`Redeemed #${pos.marketId}: +$${pos.redeemableValueUsdc.toFixed(2)}`);
} catch (err) {
console.log(`Redeem #${pos.marketId}: ${err.message}`);
}
await new Promise(r => setTimeout(r, 300));
}
// Claim creator fees
const myMarkets = await client.listMarkets({
creator: wallet.publicKey.toBase58(),
});
for (const market of myMarkets) {
const unclaimed = market.creatorFeesAccrued - market.creatorFeesClaimed;
if (unclaimed <= 0) continue;
try {
const tx = await client.buildClaimFeesTx({
marketId: market.id,
wallet: wallet.publicKey,
usdcMint,
});
await signAndSend(tx);
console.log(`Claimed $${fromRawUsdc(unclaimed).toFixed(4)} fees from #${market.id}`);
} catch {}
await new Promise(r => setTimeout(r, 300));
}
}
Part 5 — Main Loop
Tie everything together into a continuous loop:const TARGET_ACTIVE_MARKETS = 7;
async function main() {
console.log("PMX Trading Bot starting...");
console.log(`Wallet: ${wallet.publicKey.toBase58()}`);
// Run all loops concurrently
await Promise.all([
creatorLoop(),
traderLoop(),
resolverLoop(),
collectorLoop(),
]);
}
async function creatorLoop() {
while (true) {
try {
const active = await client.listMarkets({
status: "active",
creator: wallet.publicKey.toBase58(),
});
if (active.length < TARGET_ACTIVE_MARKETS) {
console.log(`[CREATOR] ${active.length}/${TARGET_ACTIVE_MARKETS} active — creating new market`);
await createMarket();
} else {
console.log(`[CREATOR] ${active.length} active — at target, skipping`);
}
} catch (err) {
console.error("[CREATOR] Error:", err.message);
}
await new Promise(r => setTimeout(r, 5 * 60 * 1000)); // every 5 min
}
}
async function traderLoop() {
while (true) {
try {
await tradeMarkets();
} catch (err) {
console.error("[TRADER] Error:", err.message);
}
await new Promise(r => setTimeout(r, 75 * 1000)); // every 75 sec
}
}
async function resolverLoop() {
while (true) {
try {
await resolveExpiredMarkets();
} catch (err) {
console.error("[RESOLVER] Error:", err.message);
}
await new Promise(r => setTimeout(r, 2 * 60 * 1000)); // every 2 min
}
}
async function collectorLoop() {
while (true) {
try {
await collectWinnings();
} catch (err) {
console.error("[COLLECTOR] Error:", err.message);
}
await new Promise(r => setTimeout(r, 20 * 60 * 1000)); // every 20 min
}
}
main().catch(console.error);
Running the Bot
Create a.env file:
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
AGENT_PRIVATE_KEY=<your-base58-private-key>
USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
COINMARKET_API_KEY=<your-cmc-api-key>
npx tsx bot.ts
Key Patterns to Remember
| Pattern | Why |
|---|---|
| Delay between transactions | Solana and RPC providers rate-limit. Add 300-500ms between calls |
| Quote before trading | Always preview with quoteBuy to check price impact before executing |
Check needsRedemption | Only redeem positions where you hold winning tokens — skip worthless losers |
getWalletPositions over getPositions | Single RPC call vs N calls — dramatically faster |
| Historical price for resolution | Use CMC historical API for the exact resolution timestamp, not current price |
| Try/catch each transaction | Individual failures shouldn’t crash the loop |
| Check unclaimed before claiming | Skip markets where creatorFeesAccrued - creatorFeesClaimed <= 0 |
Next Steps
SDK Reference
Full API reference for every method and type
Market Creation
Advanced market creation options
