feat: initial polymarket copy trade tool

- Watcher: polls Data API for trader activity
- Executor: buy/sell with market orders + retry logic
- Position manager: JSON-based state tracking
- Auto-sell: limit orders at profit target
- Redeemer: check & redeem winning positions on-chain
- Config: env-based settings with validation
- DRY_RUN mode for safe testing
This commit is contained in:
direkturcrypto
2026-02-22 15:38:13 +07:00
commit 7c7fad45f3
16 changed files with 3211 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import { Side, OrderType } from '@polymarket/clob-client';
import config from '../config/index.js';
import { getClient } from './client.js';
import { updatePosition } from './position.js';
import logger from '../utils/logger.js';
/**
* Place an auto-sell limit order at profit target above avg buy price
* @param {string} conditionId - Market condition ID
* @param {string} tokenId - CLOB token ID
* @param {number} shares - Number of shares to sell
* @param {number} avgBuyPrice - Average buy price
* @param {Object} marketOpts - { tickSize, negRisk }
*/
export async function placeAutoSell(conditionId, tokenId, shares, avgBuyPrice, marketOpts) {
try {
const profitMultiplier = 1 + config.autoSellProfitPercent / 100;
let sellPrice = avgBuyPrice * profitMultiplier;
// Round to tick size
const tickSize = parseFloat(marketOpts.tickSize);
sellPrice = Math.round(sellPrice / tickSize) * tickSize;
// Clamp between 0.01 and 0.99
sellPrice = Math.max(0.01, Math.min(0.99, sellPrice));
// Format to proper decimal places
const decimals = marketOpts.tickSize.split('.')[1]?.length || 2;
sellPrice = parseFloat(sellPrice.toFixed(decimals));
logger.trade(`Auto-sell: ${shares} shares @ $${sellPrice} (${config.autoSellProfitPercent}% above avg buy $${avgBuyPrice.toFixed(4)})`);
if (config.dryRun) {
logger.info('[DRY RUN] Would place limit sell order');
updatePosition(conditionId, { sellOrderId: 'dry-run-order' });
return;
}
const client = getClient();
const response = await client.createAndPostOrder(
{
tokenID: tokenId,
price: sellPrice,
size: shares,
side: Side.SELL,
},
{
tickSize: marketOpts.tickSize,
negRisk: marketOpts.negRisk,
},
OrderType.GTC,
);
if (response && response.success) {
logger.success(`Auto-sell order placed: ${response.orderID} @ $${sellPrice}`);
updatePosition(conditionId, { sellOrderId: response.orderID });
} else {
logger.warn(`Auto-sell failed: ${response?.errorMsg || 'Unknown'}`);
}
} catch (err) {
logger.error('Failed to place auto-sell:', err.message);
}
}
+81
View File
@@ -0,0 +1,81 @@
import { ClobClient } from '@polymarket/clob-client';
import { Wallet } from 'ethers';
import config from '../config/index.js';
import logger from '../utils/logger.js';
let clobClient = null;
let signer = null;
/**
* Initialize the Polymarket CLOB client
* Auto-derives API credentials if not provided in .env
*/
export async function initClient() {
logger.info('Initializing Polymarket CLOB client...');
signer = new Wallet(config.privateKey);
const walletAddress = signer.address;
logger.info(`Wallet address: ${walletAddress}`);
// Step 1: Create temp client to derive API credentials
let apiCreds;
if (config.clobApiKey && config.clobApiSecret && config.clobApiPassphrase) {
apiCreds = {
key: config.clobApiKey,
secret: config.clobApiSecret,
passphrase: config.clobApiPassphrase,
};
logger.info('Using API credentials from .env');
} else {
const tempClient = new ClobClient(config.clobHost, config.chainId, signer);
apiCreds = await tempClient.createOrDeriveApiKey();
logger.info('API credentials derived successfully');
}
// Step 2: Initialize full trading client
clobClient = new ClobClient(
config.clobHost,
config.chainId,
signer,
apiCreds,
0, // Signature type: 0 = EOA
config.walletAddress || walletAddress, // Funder address
);
logger.success('CLOB client initialized');
return clobClient;
}
/**
* Get the initialized CLOB client
*/
export function getClient() {
if (!clobClient) {
throw new Error('CLOB client not initialized. Call initClient() first.');
}
return clobClient;
}
/**
* Get the signer wallet
*/
export function getSigner() {
if (!signer) {
throw new Error('Signer not initialized. Call initClient() first.');
}
return signer;
}
/**
* Get USDC.e balance on Polygon for the wallet
*/
export async function getUsdcBalance() {
const { ethers } = await import('ethers');
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
const usdcAddress = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174'; // USDC.e on Polygon
const abi = ['function balanceOf(address) view returns (uint256)'];
const usdc = new ethers.Contract(usdcAddress, abi, provider);
const funderAddress = config.walletAddress || signer.address;
const balance = await usdc.balanceOf(funderAddress);
return parseFloat(ethers.utils.formatUnits(balance, 6));
}
+319
View File
@@ -0,0 +1,319 @@
import { Side, OrderType } from '@polymarket/clob-client';
import config from '../config/index.js';
import { getClient, getUsdcBalance } from './client.js';
import { hasPosition, addPosition, getPosition, updatePosition, removePosition } from './position.js';
import { fetchMarketByTokenId } from './watcher.js';
import { placeAutoSell } from './autoSell.js';
import logger from '../utils/logger.js';
/**
* Calculate trade size based on settings
* @param {number} traderSize - Trader's trade size in USDC
* @returns {number} Our trade size in USDC
*/
async function calculateTradeSize(traderSize) {
if (config.sizeMode === 'percentage') {
// % of trader's trade size
return traderSize * (config.sizePercent / 100);
} else if (config.sizeMode === 'balance') {
// % of our own balance
const balance = await getUsdcBalance();
return balance * (config.sizePercent / 100);
}
return 0;
}
/**
* Get market options (tick size and neg risk) for a token
*/
async function getMarketOptions(tokenId) {
const client = getClient();
try {
// Try to get from market info
const marketInfo = await fetchMarketByTokenId(tokenId);
if (marketInfo) {
return {
tickSize: String(marketInfo.minimum_tick_size || '0.01'),
negRisk: marketInfo.neg_risk || false,
conditionId: marketInfo.condition_id || '',
question: marketInfo.question || '',
};
}
} catch (err) {
logger.warn('Failed to get market info, using defaults:', err.message);
}
// Fallback: try SDK methods
try {
const tickSize = await client.getTickSize(tokenId);
const negRisk = await client.getNegRisk(tokenId);
return { tickSize: String(tickSize), negRisk, conditionId: '', question: '' };
} catch (err) {
logger.warn('Failed to get tick size from SDK, using default 0.01');
return { tickSize: '0.01', negRisk: false, conditionId: '', question: '' };
}
}
/**
* Execute a BUY trade (copy trader's buy)
* @param {Object} trade - Trade info from watcher
*/
export async function executeBuy(trade) {
const { tokenId, conditionId, market, price, size } = trade;
// Check if already have position for this market
if (hasPosition(conditionId)) {
logger.warn(`Already have position for: ${market || conditionId}. Skipping buy.`);
return;
}
// Calculate our trade size
const tradeSize = await calculateTradeSize(size * price); // trader's USDC amount
if (tradeSize < config.minTradeSize) {
logger.warn(`Trade size $${tradeSize.toFixed(2)} below minimum $${config.minTradeSize}. Skipping.`);
return;
}
// Check balance
const balance = await getUsdcBalance();
if (balance < tradeSize) {
logger.error(`Insufficient balance: $${balance.toFixed(2)} < $${tradeSize.toFixed(2)} needed`);
return;
}
// Get market options
const marketOpts = await getMarketOptions(tokenId);
const effectiveConditionId = conditionId || marketOpts.conditionId;
// Double check no position exists
if (effectiveConditionId && hasPosition(effectiveConditionId)) {
logger.warn(`Already have position for: ${market || effectiveConditionId}. Skipping buy.`);
return;
}
logger.trade(`BUY ${market || tokenId} | Size: $${tradeSize.toFixed(2)} | Trader price: ${price}`);
if (config.dryRun) {
logger.info('[DRY RUN] Would place market buy order');
// Still record position in dry run for testing
addPosition({
conditionId: effectiveConditionId,
tokenId,
market: market || marketOpts.question || tokenId,
shares: tradeSize / price,
avgBuyPrice: price,
totalCost: tradeSize,
outcome: trade.outcome,
});
return;
}
// Place market order with retries
const client = getClient();
let filled = false;
let totalSharesFilled = 0;
let totalCostFilled = 0;
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
try {
const remainingAmount = tradeSize - totalCostFilled;
if (remainingAmount < config.minTradeSize) break;
logger.info(`Buy attempt ${attempt}/${config.maxRetries} | Amount: $${remainingAmount.toFixed(2)}`);
// Use FAK (fill-and-kill) to get what's available, then retry remainder
const response = await client.createAndPostMarketOrder(
{
tokenID: tokenId,
side: Side.BUY,
amount: remainingAmount,
price: Math.min(price * 1.05, 0.99), // 5% slippage allowance, max 0.99
},
{
tickSize: marketOpts.tickSize,
negRisk: marketOpts.negRisk,
},
OrderType.FOK,
);
if (response && response.success) {
logger.success(`Order placed: ${response.orderID} | Status: ${response.status}`);
// Check if fully filled by trying to get trade info
const takingAmount = parseFloat(response.takingAmount || '0');
const makingAmount = parseFloat(response.makingAmount || '0');
if (takingAmount > 0 || makingAmount > 0) {
totalSharesFilled += takingAmount || (remainingAmount / price);
totalCostFilled += makingAmount || remainingAmount;
filled = true;
break; // FOK either fills fully or cancels
} else {
filled = true;
totalSharesFilled = tradeSize / price;
totalCostFilled = tradeSize;
break;
}
} else {
logger.warn(`Order not filled. Error: ${response?.errorMsg || 'Unknown'}`);
}
} catch (err) {
logger.error(`Buy attempt ${attempt} failed:`, err.message);
}
// Wait before retry
if (attempt < config.maxRetries) {
await new Promise((r) => setTimeout(r, config.retryDelay));
}
}
if (!filled || totalCostFilled === 0) {
logger.error(`Failed to fill buy order for ${market || tokenId} after ${config.maxRetries} attempts`);
return;
}
// Calculate avg buy price
const avgBuyPrice = totalSharesFilled > 0 ? totalCostFilled / totalSharesFilled : price;
// Record position
addPosition({
conditionId: effectiveConditionId,
tokenId,
market: market || marketOpts.question || tokenId,
shares: totalSharesFilled,
avgBuyPrice,
totalCost: totalCostFilled,
outcome: trade.outcome,
});
// Auto-sell if enabled
if (config.autoSellEnabled) {
await placeAutoSell(effectiveConditionId, tokenId, totalSharesFilled, avgBuyPrice, marketOpts);
}
}
/**
* Execute a SELL trade (copy trader's sell)
* @param {Object} trade - Trade info from watcher
*/
export async function executeSell(trade) {
const { tokenId, conditionId, market, price } = trade;
// Get market options to resolve conditionId
let effectiveConditionId = conditionId;
let marketOpts;
if (!effectiveConditionId) {
marketOpts = await getMarketOptions(tokenId);
effectiveConditionId = marketOpts.conditionId;
}
// Check if we have a position
const position = getPosition(effectiveConditionId);
if (!position) {
logger.warn(`No position found for: ${market || effectiveConditionId}. Skipping sell.`);
return;
}
if (position.status === 'selling' || position.status === 'sold') {
logger.warn(`Position already ${position.status}: ${market || effectiveConditionId}. Skipping.`);
return;
}
logger.trade(`SELL ${position.market} | Shares: ${position.shares} | Trader price: ${price}`);
if (config.dryRun) {
logger.info('[DRY RUN] Would place sell order');
removePosition(effectiveConditionId);
return;
}
// Cancel existing auto-sell order if any
if (position.sellOrderId) {
try {
const client = getClient();
await client.cancelOrder(position.sellOrderId);
logger.info(`Cancelled auto-sell order: ${position.sellOrderId}`);
} catch (err) {
logger.warn(`Failed to cancel auto-sell: ${err.message}`);
}
}
updatePosition(effectiveConditionId, { status: 'selling' });
if (!marketOpts) {
marketOpts = await getMarketOptions(tokenId);
}
const client = getClient();
let filled = false;
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
try {
if (config.sellMode === 'market') {
// Market sell (FOK)
logger.info(`Sell attempt ${attempt}/${config.maxRetries} (market) | Shares: ${position.shares}`);
const response = await client.createAndPostMarketOrder(
{
tokenID: tokenId,
side: Side.SELL,
amount: position.shares,
price: Math.max(price * 0.95, 0.01), // 5% slippage, min 0.01
},
{
tickSize: marketOpts.tickSize,
negRisk: marketOpts.negRisk,
},
OrderType.FOK,
);
if (response && response.success) {
logger.success(`Sell order placed: ${response.orderID}`);
filled = true;
break;
} else {
logger.warn(`Sell not filled: ${response?.errorMsg || 'Unknown'}`);
}
} else {
// Limit sell at trader's sell price
logger.info(`Sell attempt ${attempt}/${config.maxRetries} (limit) | Price: ${price}`);
const response = await client.createAndPostOrder(
{
tokenID: tokenId,
price: price,
size: position.shares,
side: Side.SELL,
},
{
tickSize: marketOpts.tickSize,
negRisk: marketOpts.negRisk,
},
OrderType.GTC,
);
if (response && response.success) {
logger.success(`Limit sell placed: ${response.orderID} @ $${price}`);
filled = true;
break;
} else {
logger.warn(`Limit sell failed: ${response?.errorMsg || 'Unknown'}`);
}
}
} catch (err) {
logger.error(`Sell attempt ${attempt} failed:`, err.message);
}
if (attempt < config.maxRetries) {
await new Promise((r) => setTimeout(r, config.retryDelay));
}
}
if (filled) {
removePosition(effectiveConditionId);
logger.money(`Position sold: ${position.market}`);
} else {
updatePosition(effectiveConditionId, { status: 'open' });
logger.error(`Failed to sell ${position.market} after ${config.maxRetries} attempts`);
}
}
+112
View File
@@ -0,0 +1,112 @@
import { readState, writeState } from '../utils/state.js';
import logger from '../utils/logger.js';
const POSITIONS_FILE = 'positions.json';
/**
* Get all current positions
* @returns {Object} Map of conditionId -> position data
*/
export function getPositions() {
return readState(POSITIONS_FILE, {});
}
/**
* Check if we already have a position for this market (conditionId)
* @param {string} conditionId
* @returns {boolean}
*/
export function hasPosition(conditionId) {
const positions = getPositions();
return !!positions[conditionId];
}
/**
* Add a new position after buy is filled
* @param {Object} params
* @param {string} params.conditionId - Market condition ID
* @param {string} params.tokenId - CLOB token ID
* @param {string} params.market - Market question/title
* @param {number} params.shares - Number of shares bought
* @param {number} params.avgBuyPrice - Average buy price
* @param {number} params.totalCost - Total USDC spent
* @param {string} params.outcome - YES/NO outcome
* @param {string} [params.sellOrderId] - Auto-sell order ID if placed
*/
export function addPosition({
conditionId,
tokenId,
market,
shares,
avgBuyPrice,
totalCost,
outcome,
sellOrderId,
}) {
const positions = getPositions();
positions[conditionId] = {
conditionId,
tokenId,
market,
shares,
avgBuyPrice,
totalCost,
outcome: outcome || '',
sellOrderId: sellOrderId || null,
status: 'open', // open, selling, sold, redeemed
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
writeState(POSITIONS_FILE, positions);
logger.success(`Position added: ${market} | ${shares} shares @ $${avgBuyPrice}`);
}
/**
* Update a position
* @param {string} conditionId
* @param {Object} updates - Fields to update
*/
export function updatePosition(conditionId, updates) {
const positions = getPositions();
if (positions[conditionId]) {
positions[conditionId] = {
...positions[conditionId],
...updates,
updatedAt: new Date().toISOString(),
};
writeState(POSITIONS_FILE, positions);
}
}
/**
* Remove a position (after sell or redeem)
* @param {string} conditionId
*/
export function removePosition(conditionId) {
const positions = getPositions();
if (positions[conditionId]) {
const market = positions[conditionId].market;
delete positions[conditionId];
writeState(POSITIONS_FILE, positions);
logger.info(`Position removed: ${market}`);
}
}
/**
* Get position by conditionId
* @param {string} conditionId
* @returns {Object|null}
*/
export function getPosition(conditionId) {
const positions = getPositions();
return positions[conditionId] || null;
}
/**
* Get all open positions as an array
* @returns {Array}
*/
export function getOpenPositions() {
const positions = getPositions();
return Object.values(positions).filter((p) => p.status === 'open');
}
+148
View File
@@ -0,0 +1,148 @@
import { ethers } from 'ethers';
import config from '../config/index.js';
import { getOpenPositions, updatePosition, removePosition } from './position.js';
import logger from '../utils/logger.js';
// Contract addresses on Polygon
const CTF_ADDRESS = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const NEG_RISK_CTF_ADDRESS = '0xC5d563A36AE78145C45a50134d48A1215220f80a';
const USDC_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
// CTF ABI (minimal for redeemPositions & balanceOf)
const CTF_ABI = [
'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)',
'function balanceOf(address owner, uint256 tokenId) view returns (uint256)',
'function payoutNumerators(bytes32 conditionId, uint256 outcomeIndex) view returns (uint256)',
'function payoutDenominator(bytes32 conditionId) view returns (uint256)',
];
/**
* Check if a market has been resolved and our position is a winner
* @param {string} conditionId
* @returns {Object|null} { resolved, won }
*/
async function checkMarketResolution(conditionId) {
try {
// Check via Gamma API
const url = `${config.gammaHost}/markets?condition_id=${conditionId}`;
const response = await fetch(url);
if (!response.ok) return null;
const markets = await response.json();
if (!markets || markets.length === 0) return null;
const market = markets[0];
return {
resolved: market.closed || market.resolved || false,
active: market.active,
endDate: market.end_date_iso,
resolutionSource: market.resolution_source,
question: market.question,
};
} catch (err) {
logger.error('Failed to check market resolution:', err.message);
return null;
}
}
/**
* Check on-chain if a position (token) has value (payout available)
*/
async function checkOnChainPayout(conditionId) {
try {
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
const ctf = new ethers.Contract(CTF_ADDRESS, CTF_ABI, provider);
const denominator = await ctf.payoutDenominator(conditionId);
if (denominator.isZero()) return { resolved: false, payouts: [] };
// Check payouts for both outcomes (YES=0, NO=1)
const payouts = [];
for (let i = 0; i < 2; i++) {
const numerator = await ctf.payoutNumerators(conditionId, i);
payouts.push(numerator.toNumber() / denominator.toNumber());
}
return { resolved: true, payouts };
} catch (err) {
// If payoutDenominator is 0 or reverts, market not resolved
return { resolved: false, payouts: [] };
}
}
/**
* Redeem winning position on-chain
*/
async function redeemPosition(conditionId, isNegRisk = false) {
try {
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
const wallet = new ethers.Wallet(config.privateKey, provider);
const ctfAddress = isNegRisk ? NEG_RISK_CTF_ADDRESS : CTF_ADDRESS;
const ctf = new ethers.Contract(ctfAddress, CTF_ABI, wallet);
const parentCollectionId = ethers.constants.HashZero;
const indexSets = [1, 2]; // Both outcomes
logger.info(`Redeeming position for conditionId: ${conditionId}`);
const tx = await ctf.redeemPositions(
USDC_ADDRESS,
parentCollectionId,
conditionId,
indexSets,
{ gasLimit: 300000 },
);
logger.info(`Redeem tx sent: ${tx.hash}`);
const receipt = await tx.wait();
logger.success(`Redeem confirmed in block ${receipt.blockNumber}`);
return true;
} catch (err) {
logger.error('Failed to redeem position:', err.message);
return false;
}
}
/**
* Check all open positions for redeemable (resolved & won) markets
*/
export async function checkAndRedeemPositions() {
const positions = getOpenPositions();
if (positions.length === 0) return;
logger.info(`Checking ${positions.length} position(s) for redemption...`);
for (const position of positions) {
try {
// Check via API first
const resolution = await checkMarketResolution(position.conditionId);
if (!resolution || !resolution.resolved) {
// Try on-chain check as fallback
const onChain = await checkOnChainPayout(position.conditionId);
if (!onChain.resolved) continue;
// Check if our outcome won
// Determine outcome index (0=YES, 1=NO based on token position)
logger.info(`Market resolved on-chain: ${position.market} | Payouts: ${onChain.payouts}`);
} else {
logger.info(`Market resolved: ${position.market}`);
}
if (config.dryRun) {
logger.info(`[DRY RUN] Would redeem position: ${position.market}`);
continue;
}
// Attempt to redeem
const success = await redeemPosition(position.conditionId);
if (success) {
removePosition(position.conditionId);
logger.money(`Redeemed: ${position.market}`);
}
} catch (err) {
logger.error(`Error checking position ${position.market}:`, err.message);
}
}
}
+137
View File
@@ -0,0 +1,137 @@
import config from '../config/index.js';
import logger from '../utils/logger.js';
import { readState, writeState } from '../utils/state.js';
const PROCESSED_FILE = 'processed_trades.json';
/**
* Fetch trader's recent activity from Data API
* @returns {Array} List of trade activities
*/
async function fetchTraderActivity() {
const url = `${config.dataHost}/activity?user=${config.traderAddress}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Data API returned ${response.status}`);
}
return await response.json();
} catch (err) {
logger.error('Failed to fetch trader activity:', err.message);
return [];
}
}
/**
* Get list of already processed trade IDs
*/
function getProcessedTrades() {
return readState(PROCESSED_FILE, { tradeIds: [] });
}
/**
* Mark a trade as processed
*/
function markTradeProcessed(tradeId) {
const data = getProcessedTrades();
data.tradeIds.push(tradeId);
// Keep only last 500 trade IDs to prevent unbounded growth
if (data.tradeIds.length > 500) {
data.tradeIds = data.tradeIds.slice(-500);
}
writeState(PROCESSED_FILE, data);
}
/**
* Check for new trades from the watched trader
* @returns {Array} New trades to process: { id, type, tokenId, conditionId, market, price, size, timestamp, side }
*/
export async function checkNewTrades() {
const activities = await fetchTraderActivity();
const processed = getProcessedTrades();
if (!Array.isArray(activities) || activities.length === 0) {
return [];
}
const newTrades = [];
for (const activity of activities) {
// Skip already processed
const tradeId = activity.id || activity.transaction_hash || `${activity.timestamp}_${activity.asset}`;
if (processed.tradeIds.includes(tradeId)) {
continue;
}
// Only process filled trades (buys and sells)
const type = activity.type?.toUpperCase();
if (!['BUY', 'SELL'].includes(type)) {
// Mark non-buy/sell as processed so we don't re-check
markTradeProcessed(tradeId);
continue;
}
// Extract trade info
const trade = {
id: tradeId,
type, // BUY or SELL
tokenId: activity.asset || activity.token_id || '',
conditionId: activity.condition_id || activity.conditionId || '',
market: activity.title || activity.question || activity.market || '',
price: parseFloat(activity.price || '0'),
size: parseFloat(activity.size || activity.amount || '0'),
side: activity.side || type,
timestamp: activity.timestamp || activity.created_at || new Date().toISOString(),
outcome: activity.outcome || '',
proxyWalletAddress: activity.proxyWalletAddress || '',
};
// Need tokenId to trade
if (!trade.tokenId) {
logger.warn(`Skipping trade without tokenId: ${tradeId}`);
markTradeProcessed(tradeId);
continue;
}
newTrades.push(trade);
}
return newTrades;
}
/**
* Mark trade as processed after handling
*/
export { markTradeProcessed };
/**
* Fetch market info from Gamma API by condition ID
*/
export async function fetchMarketInfo(conditionId) {
try {
const url = `${config.gammaHost}/markets?condition_id=${conditionId}`;
const response = await fetch(url);
if (!response.ok) return null;
const markets = await response.json();
return markets && markets.length > 0 ? markets[0] : null;
} catch (err) {
logger.error('Failed to fetch market info:', err.message);
return null;
}
}
/**
* Fetch market info by token ID (CLOB token)
*/
export async function fetchMarketByTokenId(tokenId) {
try {
const url = `${config.gammaHost}/markets?clob_token_ids=${tokenId}`;
const response = await fetch(url);
if (!response.ok) return null;
const markets = await response.json();
return markets && markets.length > 0 ? markets[0] : null;
} catch (err) {
logger.error('Failed to fetch market by tokenId:', err.message);
return null;
}
}