Overview
The Polymarket Bot integrates three external data sources:
- ChainlinkFeed - Real-time BTC/USD price stream via WebSocket
- Vatic API - 5-minute strike (target) prices for binary options
- 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}`)
}
})
Configuration optionsCalled with { timestamp: number, price: number } every time a new BTC/USD price arrives. Timestamp is in milliseconds (Date.now()).
Called with status string: 'connected' | 'disconnected' | 'reconnecting'
Methods
start()
Opens the WebSocket connection and begins streaming prices.
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.
Side effects:
- Clears any pending reconnect timer
- Closes WebSocket connection
- Removes all event listeners
- Calls
onStatus('disconnected')
isConnected()
Check current connection state.
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.
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.
Unix timestamp in seconds. Should be aligned to a 5-minute boundary (divisible by 300) for meaningful data.
Strike price resultThe strike price in USD, or null if unavailable. Extracted from target_price, target, or price fields in API response.
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×tamp={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.
Unix seconds aligned to 5-minute boundary
Market discovery resultTrue if an active market was discovered
Polymarket condition ID (present when found=true)
CLOB token ID for the “Up” outcome (present when found=true)
CLOB token ID for the “Down” outcome (present when found=true)
Initial outcome prices [upPrice, downPrice] (present when found=true)
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:
- Primary slug:
btc-updown-5m-{epochTimestamp} (e.g., btc-updown-5m-1733227500)
- Fallback slugs: ±300s offsets (configurable via
config.feeds.polymarket.offsetsFallback)
- 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).
CLOB token ID for the “Up” outcome (from discoverActiveMarket)
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.
Model’s predicted probability of “Up” (0-1)
Market’s implied probability of “Up” (0-1), i.e., price of UP token
EV calculation result, or null if inputs are degenerateExpected value of buying YES token: p/q - 1
Expected value of buying NO token: (1-p)/(1-q) - 1
Safety margin: |p - q| / max(p, 1-p)
Alias for bestEV (used in main bot logic)
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