Skip to main content

Overview

The prediction engine aggregates data from four independent sources:

Chainlink WebSocket

Live BTC/USD price stream (1-second frequency)

Vatic Trading API

Strike prices for 5-minute intervals

Polymarket Gamma

Market discovery and metadata

Polymarket CLOB

Live market prices (implied probabilities)

Connection Details

Endpoint: wss://ws-live-data.polymarket.com Subscription Message:
{
  "action": "subscribe",
  "subscriptions": [
    {
      "topic": "crypto_prices_chainlink",
      "type": "*",
      "filters": "{\"symbol\": \"btc/usd\"}"
    }
  ]
}
Response Format:
{
  "topic": "crypto_prices_chainlink",
  "payload": {
    "symbol": "btc/usd",
    "value": "64232.02",
    "timestamp": 1709571234567
  }
}

Implementation

From src/feeds/chainlink.js:79-117:
export class ChainlinkFeed {
  constructor({ onTick, onStatus }) {
    this._onTick = onTick
    this._onStatus = onStatus
    this._ws = null
    this._reconnectTimer = null
    this._running = false
    this._lastValidPrice = null
  }

  start() {
    if (this._running) return
    this._running = true
    this._connect()
  }

  _connect() {
    this._ws = new WebSocket('wss://ws-live-data.polymarket.com')

    this._ws.on('open', () => {
      this._onStatus('connected')
      this._ws.send(SUBSCRIPTION_MSG)
    })

    this._ws.on('message', (msg) => {
      const data = JSON.parse(msg.toString())
      if (data.topic !== 'crypto_prices_chainlink') return
      if (data.payload.symbol !== 'btc/usd') return

      const price = Number(data.payload.value)

      // Validation (see below)
      if (!this._validateTick(price)) return

      this._onTick({ timestamp: Date.now(), price })
    })

    this._ws.on('close', () => {
      this._ws = null
      if (!this._running) return

      this._onStatus('reconnecting')
      this._reconnectTimer = setTimeout(() => {
        if (this._running) this._connect()
      }, 3000)  // RECONNECT_DELAY_MS
    })
  }
}

Data Validation

Two guards protect against bad data: 1. NaN / Zero Guard
if (!Number.isFinite(price) || price <= 0) {
  logger.warn('Invalid tick rejected', { price, reason: 'not finite or <= 0' })
  return
}
2. Spike Filter (10% threshold)
const spikeThreshold = config.feeds.chainlink.spikeThreshold  // 0.10
if (this._lastValidPrice !== null && 
    Math.abs(price / this._lastValidPrice - 1) > spikeThreshold) {
  logger.warn('Spike tick rejected', {
    price,
    lastPrice: this._lastValidPrice,
    deviation: ((price / this._lastValidPrice - 1) * 100).toFixed(2) + '%'
  })
  return
}
this._lastValidPrice = price
This prevents:
  • Oracle glitches (e.g., price reported as 0 or NaN)
  • Flash crash artifacts (10%+ instant moves)
  • Data corruption in transit
The spike filter can reject legitimate moves during extreme volatility. For BTC, a 10% move in 1 second is exceedingly rare but possible. Adjust feeds.chainlink.spikeThreshold if needed.

Reconnection Logic

When the WebSocket disconnects:
  1. Close event fires → set status to ‘reconnecting’
  2. Wait 3 seconds (configurable delay)
  3. Attempt reconnection via _connect()
  4. Repeat indefinitely while _running = true
Graceful shutdown:
stop() {
  this._running = false
  if (this._reconnectTimer) clearTimeout(this._reconnectTimer)
  if (this._ws) {
    this._ws.removeAllListeners()
    this._ws.close()
  }
  this._onStatus('disconnected')
}

Vatic Trading API

Endpoint

Base URL: https://api.vatic.trading/api/v1/targets/timestamp Query Parameters:
  • asset=btc
  • type=5min
  • timestamp={epoch} (Unix seconds, aligned to 5-min boundary)
Example Request:
curl "https://api.vatic.trading/api/v1/targets/timestamp?asset=btc&type=5min&timestamp=1709571300"
Response Format:
{
  "target_price": 64355.45,
  "timestamp": 1709571300,
  "asset": "btc",
  "interval": "5min"
}

Implementation

From src/feeds/vatic.js:14-41:
export function getCurrent5mTimestamp() {
  const nowSec = Math.floor(Date.now() / 1000)
  return Math.floor(nowSec / 300) * 300  // Align to 5-min boundary
}

export async function fetchStrikePrice(epochTimestamp) {
  const url = `${VATIC_BASE}?asset=btc&type=5min&timestamp=${epochTimestamp}`
  const res = await fetch(url)

  if (!res.ok) {
    throw new Error(`Vatic API HTTP ${res.status} ${res.statusText}`)
  }

  const json = await res.json()
  const strikePrice = json.target_price ?? json.target ?? json.price ?? null

  return { strikePrice, rawData: json }
}

Caching Strategy

Strike prices are cached per epoch in src/index.js:68-96:
let cachedStrike = { epoch: null, price: null, rawData: null }

// Fetch only when epoch changes
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
This reduces API calls from 300/interval (once per second) to 1/interval (once per epoch).
Vatic API typically updates strike prices 10-30 seconds before the interval starts. If fetching too early (e.g., T-290s), the API may return the previous interval’s strike.

Polymarket Gamma API

Market Discovery

Base URL: https://gamma-api.polymarket.com Endpoint: /events?slug={slug} Slug Pattern: btc-updown-5m-{epochTimestamp} Example: btc-updown-5m-1709571300 for the 5-minute market starting at epoch 1709571300.

Implementation

From src/feeds/polymarket.js:47-98:
export async function discoverActiveMarket(epochTimestamp) {
  const slugs = [
    SLUG_PREFIX + epochTimestamp,
    ...OFFSETS_FALLBACK.map(offset => SLUG_PREFIX + (epochTimestamp + offset)),
  ]
  // slugs = ['btc-updown-5m-1709571300', 'btc-updown-5m-1709571000', 'btc-updown-5m-1709571600']

  for (const slug of slugs) {
    const url = `${GAMMA_BASE}/events?slug=${slug}`
    const res = await _throttledFetch(url)  // Rate-limited

    if (!res.ok) continue

    const data = await res.json()
    if (!data || data.length === 0) continue

    const event = data[0]
    const market = event.markets?.[0]
    if (!market) continue

    const tokenIds = JSON.parse(market.clobTokenIds || '[]')
    const outcomes = JSON.parse(market.outcomes || '[]')

    // Determine which token is "Up" vs "Down"
    const upIndex = outcomes.indexOf('Up')
    const downIndex = outcomes.indexOf('Down')

    if (upIndex === -1 || downIndex === -1 || tokenIds.length < 2) {
      logger.warn('Unexpected market structure', { outcomes, tokenIds: tokenIds.length, slug })
      continue
    }

    return {
      found: true,
      conditionId: market.conditionId,
      upTokenId: tokenIds[upIndex],
      downTokenId: tokenIds[downIndex],
      outcomePrices: JSON.parse(market.outcomePrices || '[]').map(Number),
      slug,
    }
  }

  return { found: false }
}

Fallback Offsets

If the primary slug returns no results, the engine tries adjacent epochs (±300s):
{
  "feeds": {
    "polymarket": {
      "offsetsFallback": [-300, 300]
    }
  }
}
This handles:
  • Clock skew between local time and Polymarket servers
  • Markets created early/late
  • Slug naming inconsistencies

Rate Limiting

Gamma API requests are throttled to 1 request per second:
let _lastRequestTime = 0
const MIN_INTERVAL_MS = 1000

async function _throttledFetch(url) {
  const now = Date.now()
  const elapsed = now - _lastRequestTime
  if (elapsed < MIN_INTERVAL_MS) {
    await new Promise(r => setTimeout(r, MIN_INTERVAL_MS - elapsed))
  }
  _lastRequestTime = Date.now()
  return fetch(url)
}

Polymarket CLOB API

Market Price Endpoint

Base URL: https://clob.polymarket.com Endpoint: /price?token_id={tokenId}&side=BUY Example Request:
curl "https://clob.polymarket.com/price?token_id=0x1234...&side=BUY"
Response Format:
{
  "price": "0.1523",
  "token_id": "0x1234..."
}
The price field represents the market’s implied probability of the “Up” outcome.

Implementation

From src/feeds/polymarket.js:109-132:
export async function getMarketPrice(tokenId) {
  try {
    const url = `${CLOB_BASE}/price?token_id=${tokenId}&side=BUY`
    const res = await _throttledFetch(url)

    if (!res.ok) {
      logger.warn('CLOB price non-OK response', { status: res.status, tokenId })
      return null
    }

    const data = await res.json()
    const price = parseFloat(data.price)

    // Guard: reject degenerate values
    if (!Number.isFinite(price) || price <= 0 || price >= 1) {
      return null
    }

    return price
  } catch (e) {
    logger.warn('CLOB price fetch error', { tokenId, error: e.message })
    return null
  }
}

Polling Strategy

Market prices are polled every 5 seconds (configurable via feeds.polymarket.pollIntervalSec):
const polyPollSec = config.feeds?.polymarket?.pollIntervalSec ?? 5
let cachedQMarket = { value: null, lastFetchSec: 0 }

// Poll price every polyPollSec
if (cachedMarket.data && (nowSec - cachedQMarket.lastFetchSec) >= polyPollSec) {
  const price = await getMarketPrice(cachedMarket.data.upTokenId)
  if (price != null) {
    cachedQMarket = { value: price, lastFetchSec: nowSec }
  }
}
Why not use the /book endpoint? The order book can contain stale orders that don’t reflect current market conditions. The /price endpoint returns the best executable price.

Expected Value Calculation

Formula

Comparing model probability p against market probability q:
EV(YES) = p/q - 1
EV(NO)  = (1-p)/(1-q) - 1

Margin = |p - q| / max(p, 1-p)
Edge   = p - q

Implementation

From src/feeds/polymarket.js:151-161:
export function calculateEV(p, q) {
  if (q <= 0 || q >= 1 || p <= 0 || p >= 1) return null

  const evYes = p / q - 1
  const evNo = (1 - p) / (1 - q) - 1
  const margin = Math.abs(p - q) / Math.max(p, 1 - p)
  const bestSide = evYes > evNo ? 'YES' : 'NO'
  const bestEV = Math.max(evYes, evNo)

  return { evYes, evNo, margin, bestSide, bestEV, ev: bestEV, edge: p - q }
}

Example

// Model predicts 85% UP, market prices UP at 10%
const ev = calculateEV(0.85, 0.10)

/*
evYes = 0.85 / 0.10 - 1 = 7.50  (+750% EV on YES bet)
evNo  = (1-0.85) / (1-0.10) - 1 = 0.15/0.90 - 1 = -0.833  (-83% EV on NO bet)
margin = |0.85 - 0.10| / max(0.85, 0.15) = 0.75 / 0.85 = 0.88  (88% safety margin)
edge = 0.85 - 0.10 = 0.75  (75pp edge)
bestSide = 'YES'
bestEV = 7.50
*/
Extreme EV values (> 5.00) often indicate:
  • Stale market prices
  • Low liquidity (wide spreads)
  • Oracle delays
  • Model miscalibration
Always verify market conditions before trading high-EV opportunities.

Data Flow Timeline

Data Quality Checks

  • HTTP status check (reject non-2xx)
  • Epoch alignment verification
  • Multiple field fallbacks (target_price || target || price)
  • Per-epoch caching prevents redundant requests
  • Fallback slug offsets (±300s)
  • Market structure validation (requires upIndex, downIndex)
  • Token ID array bounds check
  • Rate limiting (1 req/sec)
  • Price bounds check (0 < q < 1)
  • Finite number validation
  • 5-second polling cadence
  • Error logs for failed fetches (non-blocking)

Configuration

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

Next Steps

How Predictions Work

Understand how raw data becomes calibrated probabilities

Risk Management

Learn how bet sizing and drawdown tracking work

Build docs developers (and LLMs) love