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)
Chainlink WebSocket Feed
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:
Close event fires → set status to ‘reconnecting’
Wait 3 seconds (configurable delay)
Attempt reconnection via _connect()
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×tamp=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×tamp= ${ 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
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
NaN / zero guard
10% spike filter
Auto-reconnect on disconnect
Frame-level JSON parsing errors logged
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