Skip to main content

Overview

The Polymarket Bot integrates three external data sources:
  1. ChainlinkFeed - Real-time BTC/USD price stream via WebSocket
  2. Vatic API - 5-minute strike (target) prices for binary options
  3. Polymarket API - Market discovery and implied probability prices
Each feed is designed for resilience, automatic reconnection, and rate limiting.

ChainlinkFeed

WebSocket client for Polymarket’s Chainlink BTC/USD real-time price stream. Source: src/feeds/chainlink.js:22

Constructor

import { ChainlinkFeed } from './feeds/chainlink.js'

const feed = new ChainlinkFeed({
  onTick: (tick) => {
    console.log(`Price: ${tick.price} at ${tick.timestamp}`)
  },
  onStatus: (status) => {
    console.log(`Connection: ${status}`)
  }
})
opts
object
required
Configuration options
opts.onTick
function
required
Called with { timestamp: number, price: number } every time a new BTC/USD price arrives. Timestamp is in milliseconds (Date.now()).
opts.onStatus
function
required
Called with status string: 'connected' | 'disconnected' | 'reconnecting'

Methods

start()

Opens the WebSocket connection and begins streaming prices.
returns
void
No return value. Idempotent - calling start() while already running is a no-op.
Example:
// From src/index.js:52-62
const feed = new ChainlinkFeed({
  onTick: (tick) => {
    tickLogger.write(tick)
    lastPrice = tick.price
    engine.feedTick(tick)
  },
  onStatus: (status) => {
    connectionStatus = status
  }
})
feed.start()
Behavior:
  • Connects to wss://ws-live-data.polymarket.com
  • Subscribes to crypto_prices_chainlink topic with filter { symbol: 'btc/usd' }
  • Calls onTick() for every valid price update
  • Calls onStatus('connected') on successful connection
  • Automatically reconnects on disconnect (3-second delay)

stop()

Gracefully shuts down the feed.
returns
void
No return value.
Side effects:
  • Clears any pending reconnect timer
  • Closes WebSocket connection
  • Removes all event listeners
  • Calls onStatus('disconnected')

isConnected()

Check current connection state.
returns
boolean
True when the underlying WebSocket is in the OPEN state, false otherwise.
Example:
if (feed.isConnected()) {
  console.log('Ready to receive prices')
}

Spike Protection

The feed validates incoming ticks and rejects:
  • Non-finite or negative prices
  • Spike prices that deviate >10% from the last valid price (configurable via config.feeds.chainlink.spikeThreshold)
Source: src/feeds/chainlink.js:98-111 Invalid ticks are logged but do not trigger onTick() callbacks.

Configuration

{
  "feeds": {
    "chainlink": {
      "spikeThreshold": 0.10  // Reject ticks deviating >10% from last valid price
    }
  }
}

Vatic API

Pure functions for fetching BTC 5-minute strike (target) prices. Source: src/feeds/vatic.js

getCurrent5mTimestamp()

Returns the current Unix timestamp aligned to the most recent 5-minute boundary.
returns
number
Epoch seconds, divisible by 300. Example: if now is 12:07:33 UTC, returns the epoch for 12:05:00 UTC.
Example:
import { getCurrent5mTimestamp } from './feeds/vatic.js'

const epochTs = getCurrent5mTimestamp()
console.log(epochTs)  // e.g., 1733227500 (2024-12-03 12:05:00 UTC)
Source: src/feeds/vatic.js:14 Implementation:
export function getCurrent5mTimestamp() {
  const nowSec = Math.floor(Date.now() / 1000)
  return Math.floor(nowSec / 300) * 300
}

fetchStrikePrice()

Fetches the BTC 5-minute strike (target) price for a given epoch timestamp.
epochTimestamp
number
required
Unix timestamp in seconds. Should be aligned to a 5-minute boundary (divisible by 300) for meaningful data.
returns
Promise<object>
Strike price result
strikePrice
number | null
The strike price in USD, or null if unavailable. Extracted from target_price, target, or price fields in API response.
rawData
object
Full JSON response from Vatic API for debugging/logging
Throws: Error on non-2xx HTTP responses (message includes status code) Example:
import { getCurrent5mTimestamp, fetchStrikePrice } from './feeds/vatic.js'

const epochTs = getCurrent5mTimestamp()
try {
  const { strikePrice, rawData } = await fetchStrikePrice(epochTs)
  console.log(`Strike for ${epochTs}: $${strikePrice}`)
} catch (error) {
  console.error(`Vatic API error: ${error.message}`)
}
From src/index.js:
// Fetch strike (cached per epoch)
if (epochTs !== cachedStrike.epoch) {
  try {
    const result = await fetchStrikePrice(epochTs)
    cachedStrike = { 
      epoch: epochTs, 
      price: result.strikePrice, 
      rawData: result.rawData 
    }
  } catch (e) { 
    logger.warn('Vatic strike fetch failed', { 
      epoch: epochTs, 
      error: e.message 
    }) 
  }
}
const strikePrice = cachedStrike.price
API endpoint: https://api.vatic.trading/api/v1/targets/timestamp?asset=btc&type=5min&timestamp={epochTimestamp} Source: src/feeds/vatic.js:27
Cache strike prices per epoch - they don’t change within a 5-minute interval. See src/index.js:90-96 for caching pattern.

Polymarket API

Functions for discovering active BTC 5-minute binary markets and fetching live market prices. Source: src/feeds/polymarket.js

discoverActiveMarket()

Discover the active BTC 5-minute binary market for a given epoch. Constructs the slug from the epoch timestamp and queries Gamma API. Falls back to offset timestamps (±300s) if the primary slug returns empty.
epochTimestamp
number
required
Unix seconds aligned to 5-minute boundary
returns
Promise<object>
Market discovery result
found
boolean
True if an active market was discovered
conditionId
string
Polymarket condition ID (present when found=true)
upTokenId
string
CLOB token ID for the “Up” outcome (present when found=true)
downTokenId
string
CLOB token ID for the “Down” outcome (present when found=true)
outcomePrices
number[]
Initial outcome prices [upPrice, downPrice] (present when found=true)
slug
string
Market slug that was successfully matched (present when found=true)
Example:
import { discoverActiveMarket } from './feeds/polymarket.js'
import { getCurrent5mTimestamp } from './feeds/vatic.js'

const epochTs = getCurrent5mTimestamp()
const market = await discoverActiveMarket(epochTs)

if (market.found) {
  console.log(`Market found: ${market.slug}`)
  console.log(`Up token: ${market.upTokenId}`)
  console.log(`Down token: ${market.downTokenId}`)
} else {
  console.log('No active market for this epoch')
}
From src/index.js:
// Discover market once per epoch
if (epochTs !== cachedMarket.epoch) {
  try {
    const mkt = await discoverActiveMarket(epochTs)
    cachedMarket = { 
      epoch: epochTs, 
      data: mkt.found ? mkt : null 
    }
    cachedQMarket = { value: null, lastFetchSec: 0 }
    polymarketStatus = mkt.found ? 'connected' : 'disconnected'
  } catch (e) {
    logger.warn('Polymarket discovery failed', { 
      epoch: epochTs, 
      error: e.message 
    })
    cachedMarket = { epoch: epochTs, data: null }
    polymarketStatus = 'disconnected'
  }
}
Source: src/feeds/polymarket.js:47 Discovery strategy:
  1. Primary slug: btc-updown-5m-{epochTimestamp} (e.g., btc-updown-5m-1733227500)
  2. Fallback slugs: ±300s offsets (configurable via config.feeds.polymarket.offsetsFallback)
  3. Returns first successful match
Rate limiting: 1-second minimum interval between requests (throttled internally)

getMarketPrice()

Fetch the current market price (implied probability) for a token. Uses CLOB /price endpoint (NOT /book, which can return stale data).
tokenId
string
required
CLOB token ID for the “Up” outcome (from discoverActiveMarket)
returns
Promise<number | null>
Price in (0,1) representing implied probability, or null if unavailable/degenerate
Example:
import { getMarketPrice } from './feeds/polymarket.js'

const market = await discoverActiveMarket(epochTs)
if (market.found) {
  const qMarket = await getMarketPrice(market.upTokenId)
  if (qMarket != null) {
    console.log(`Market implies ${(qMarket * 100).toFixed(1)}% chance of UP`)
  }
}
From src/index.js:
// Poll price every polyPollSec
if (cachedMarket.data && (nowSec - cachedQMarket.lastFetchSec) >= polyPollSec) {
  try {
    const price = await getMarketPrice(cachedMarket.data.upTokenId)
    if (price != null) {
      cachedQMarket = { value: price, lastFetchSec: nowSec }
      qMarket = price
      polymarketStatus = 'connected'
    }
  } catch (e) {
    logger.warn('Polymarket price fetch failed', { error: e.message })
  }
}
Source: src/feeds/polymarket.js:109 Degenerate value guards: Returns null for:
  • Non-finite prices
  • Prices ≤ 0 or ≥ 1
Rate limiting: 1-second minimum interval between requests (shared with discoverActiveMarket)
Poll market prices every 5 seconds (configurable via config.feeds.polymarket.pollIntervalSec). Don’t fetch on every tick - rate limits apply.

calculateEV()

Calculate Expected Value comparing model probability p against market probability q.
p
number
required
Model’s predicted probability of “Up” (0-1)
q
number
required
Market’s implied probability of “Up” (0-1), i.e., price of UP token
returns
object | null
EV calculation result, or null if inputs are degenerate
evYes
number
Expected value of buying YES token: p/q - 1
evNo
number
Expected value of buying NO token: (1-p)/(1-q) - 1
margin
number
Safety margin: |p - q| / max(p, 1-p)
bestSide
'YES' | 'NO'
Which side has higher EV
bestEV
number
Higher of evYes and evNo
ev
number
Alias for bestEV (used in main bot logic)
edge
number
Model edge: p - q (positive when model is more bullish than market)
Example:
import { calculateEV } from './feeds/polymarket.js'

const p = 0.65  // Model predicts 65% chance of UP
const q = 0.55  // Market prices UP at 55 cents

const ev = calculateEV(p, q)
if (ev) {
  console.log(`Best side: ${ev.bestSide}`)
  console.log(`Expected value: ${(ev.bestEV * 100).toFixed(2)}%`)
  console.log(`Edge: ${(ev.edge * 100).toFixed(2)}%`)
  console.log(`Margin: ${(ev.margin * 100).toFixed(2)}%`)
}
From src/index.js:
// Post-prediction EV filter
let evResult = null
if (prediction && qMarket != null) {
  evResult = calculateEV(prediction.probability, qMarket)
  if (evResult) {
    const absCfg = config.engine.abstention
    if (evResult.bestEV < absCfg.minEV) {
      abstention = { 
        abstained: true, 
        reason: 'insufficient_ev', 
        volatility: prediction.volatility 
      }
      prediction = null
    } else if (evResult.margin < absCfg.minMargin) {
      abstention = { 
        abstained: true, 
        reason: 'insufficient_margin', 
        volatility: prediction.volatility 
      }
      prediction = null
    }
  }
}
Source: src/feeds/polymarket.js:151 Degenerate inputs: Returns null when q ≤ 0, q ≥ 1, p ≤ 0, or p ≥ 1

Configuration

{
  "feeds": {
    "polymarket": {
      "gammaBaseUrl": "https://gamma-api.polymarket.com",
      "clobBaseUrl": "https://clob.polymarket.com",
      "slugPrefix": "btc-updown-5m-",
      "pollIntervalSec": 5,
      "offsetsFallback": [-300, 300],
      "enabled": true
    }
  }
}

Integration Example

import { ChainlinkFeed } from './feeds/chainlink.js'
import { getCurrent5mTimestamp, fetchStrikePrice } from './feeds/vatic.js'
import { discoverActiveMarket, getMarketPrice, calculateEV } from './feeds/polymarket.js'
import { PredictionEngine } from './engine/predictor.js'

const engine = new PredictionEngine()
let lastPrice = null
let cachedStrike = { epoch: null, price: null }
let cachedMarket = { epoch: null, data: null }
let qMarket = null

// Start price feed
const feed = new ChainlinkFeed({
  onTick: (tick) => {
    lastPrice = tick.price
    engine.feedTick(tick)
  },
  onStatus: (status) => console.log(`Chainlink: ${status}`)
})
feed.start()

// Main loop
while (true) {
  const epochTs = getCurrent5mTimestamp()
  const nowSec = Math.floor(Date.now() / 1000)
  
  // Fetch strike (once per epoch)
  if (epochTs !== cachedStrike.epoch) {
    const { strikePrice } = await fetchStrikePrice(epochTs)
    cachedStrike = { epoch: epochTs, price: strikePrice }
  }
  
  // Discover market (once per epoch)
  if (epochTs !== cachedMarket.epoch) {
    const market = await discoverActiveMarket(epochTs)
    cachedMarket = { epoch: epochTs, data: market.found ? market : null }
  }
  
  // Poll market price (every 5 seconds)
  if (cachedMarket.data) {
    qMarket = await getMarketPrice(cachedMarket.data.upTokenId)
  }
  
  // Make prediction
  if (lastPrice && cachedStrike.price) {
    const result = engine.predict({
      currentPrice: lastPrice,
      strikePrice: cachedStrike.price,
      timeRemainingSeconds: (epochTs + 300) - nowSec
    })
    
    if (!result.abstained && qMarket) {
      const ev = calculateEV(result.probability, qMarket)
      if (ev && ev.bestEV > 0.05) {
        console.log(`Trade signal: ${ev.bestSide} (EV: ${(ev.bestEV * 100).toFixed(2)}%)`)
      }
    }
  }
  
  await new Promise(r => setTimeout(r, 1000))
}

See Also

Build docs developers (and LLMs) love