GuidesMarket data
TradingView charts
What you’re building
Section titled “What you’re building”An Advanced Charts datafeed has two data paths: historical bars from
fetchOHLCV, then updates to the forming bar from the trades WebSocket channel.
The chart integration stays the same across venues, but live-trade availability does
not. Switch venue and supply that venue’s native outcomeId / marketId to change
the market source, then check the per-venue table below.
This guide wires one configured outcome into the five datafeed methods Advanced Charts
calls: onReady, resolveSymbol, getBars, subscribeBars, and unsubscribeBars.
Datafeed contract
Section titled “Datafeed contract”Advanced Charts names these resolutions 1S, 5S, 10S, 30S, 1, 5, 15, 30,
60, 240, 360, and 1D. Predictefy’s matching server resolutions are 1s, 5s,
10s, 30s, 1m, 5m, 15m, 30m, 1h, 4h, 6h, and 1d:
const SERVER_RESOLUTION = { '1S': '1s', '5S': '5s', '10S': '10s', '30S': '30s', '1': '1m', '5': '5m', '15': '15m', '30': '30m', '60': '1h', '240': '4h', '360': '6h', '1D': '1d',} as const;
const BAR_MS = { '1S': 1_000, '5S': 5_000, '10S': 10_000, '30S': 30_000, '1': 60_000, '5': 300_000, '15': 900_000, '30': 1_800_000, '60': 60 * 60_000, '240': 14_400_000, '360': 21_600_000, '1D': 24 * 60 * 60_000,} as const;
const DATAFEED_CONFIGURATION = { supported_resolutions: ['1S', '5S', '10S', '30S', '1', '5', '15', '30', '60', '240', '360', '1D'],};
function createPredictefyDatafeed(options) { const getBars = createGetBars(options); const { subscribeBars, unsubscribeBars } = createLiveBars(options);
return { onReady(callback) { setTimeout(() => callback(DATAFEED_CONFIGURATION), 0); }, resolveSymbol(_symbolName, onResolved) { setTimeout(() => onResolved(options.symbolInfo), 0); }, getBars, subscribeBars, unsubscribeBars, };}Pass symbolInfo in the shape required by your licensed Advanced Charts build, with the
same supported_resolutions. This factory represents one outcome, so every symbol lookup
resolves to that configured object.
| Venue | Sub-minute outcomeId format | Example |
|---|---|---|
| polymarket | CLOB asset id | 11198861… |
| kalshi | ticker (YES view) / ticker-NO (NO view) | KXBTC15M-…-00 |
| hyperliquid | full HIP-4 asset id (venue coin uses #N) | 100001730 (#1730) |
| sxbet | marketHash#outcomeIndex | 0x3f…9a#0 |
| myriad | networkId:marketId:outcomeIdx | 42220:1320:0 |
History via fetchOHLCV
Section titled “History via fetchOHLCV”The upstream REST endpoint is
GET https://data.predictefy.com/api/{venue}/fetchOHLCV. Your server calls it with a
server-side API key. The endpoint accepts the venue-native outcomeId, resolution,
optional start / end as ISO timestamps or epoch milliseconds, and an optional
limit of up to 5,000 candles.
Proxy history through your server
Section titled “Proxy history through your server”Response compression is rolling out on data.predictefy.com from 2026-10-05: JSON and NDJSON responses of 1,024 bytes or more are compressed (gzip or deflate) when the client sends Accept-Encoding (the SDKs do this automatically); exec.predictefy.com does not compress responses. Make a proxy safe before it reaches you.
Any HTTP client that decompresses transparently (Node.js fetch, undici, node-fetch, Python requests, httpx, aiohttp) returns decoded bodies but retains upstream Content-Encoding and Content-Length.
Before re-emitting a response, drop both headers, re-serialize parsed JSON without upstream headers, or request Accept-Encoding: identity.
Set PREDICTEFY_API_KEY in your server environment, then mount this Express route:
import express from 'express';
const app = express();if (!process.env.PREDICTEFY_API_KEY) throw new Error('PREDICTEFY_API_KEY is required');
const FORWARDED_PARAMS = ['outcomeId', 'resolution', 'start', 'end', 'limit'] as const;
app.get('/api/predictefy/ohlcv', async (req, res) => { const venue = typeof req.query.venue === 'string' ? req.query.venue : ''; if (!venue) return res.status(400).json({ error: 'venue is required' });
const url = new URL( `/api/${encodeURIComponent(venue)}/fetchOHLCV`, 'https://data.predictefy.com', ); for (const name of FORWARDED_PARAMS) { const value = req.query[name]; if (typeof value === 'string') url.searchParams.set(name, value); }
const upstream = await fetch(url, { headers: { Authorization: `Bearer ${process.env.PREDICTEFY_API_KEY}` }, }); const body = await upstream.json(); return res.status(upstream.status).json(body);});The browser datafeed calls that route on its own origin. periodParams.from and
periodParams.to are converted from seconds to epoch milliseconds. countBack becomes
limit, capped at 5,000:
type HistoryCandle = { timestamp: number; open: number; high: number; low: number; close: number; volume: number | null; source: 'official' | 'onchain' | 'write-forward' | 'derived'; sourceType: 'true-candle' | 'point-derived' | 'trade-derived' | 'book-derived' | 'rollup'; quality: 'ok' | 'partial' | 'suspect' | 'mixed'; isTrueCandle: boolean;};
type HistoryResponse = { data: HistoryCandle[]; meta?: unknown;};
function createGetBars({ venue, outcomeId, onHistoryMeta = () => undefined }) { return async function getBars( _symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback, ) { const serverResolution = SERVER_RESOLUTION[resolution]; if (!serverResolution) { onErrorCallback(`Unsupported resolution: ${resolution}`); return; }
const params = new URLSearchParams({ venue, outcomeId, resolution: serverResolution, start: String(periodParams.from * 1000), end: String(periodParams.to * 1000), limit: String(Math.max(1, Math.min(periodParams.countBack, 5000))), });
try { const response = await fetch(`/api/predictefy/ohlcv?${params.toString()}`); if (!response.ok) throw new Error(`History request failed: HTTP ${response.status}`);
const payload = (await response.json()) as HistoryResponse; if (!Array.isArray(payload.data)) throw new Error('History response has no data array'); onHistoryMeta(payload.meta);
const bars = payload.data .map((row) => ({ time: row.timestamp, open: row.open, high: row.high, low: row.low, close: row.close, volume: row.volume ?? 0, })) .sort((a, b) => a.time - b.time);
onHistoryCallback(bars, { noData: bars.length === 0 }); } catch (error) { onErrorCallback(error instanceof Error ? error.message : 'History request failed'); } };}The response meta carries request-level provenance. Each row also keeps its own
source, sourceType, quality, and isTrueCandle fields, even though Advanced Charts
only needs the OHLCV fields above. Page a longer range with start / end; one call never
returns more than 5,000 candles.
Live bars from the trades channel
Section titled “Live bars from the trades channel”Browser clients connect to the WebSocket origin shown in the dashboard, using the
/v1/stream path. The first frame authenticates; the second subscribes to the
venue-native market id:
For a direct browser connection, create a dedicated key for this page with only the read
scope. The key must not carry trade or sql. Because the page receives it, treat it as
disclosed and revocable, and never reuse the server-side history key:
{ "op": "auth", "apiKey": "pk_live_…" }{ "op": "subscribe", "channel": "trades", "venue": "polymarket", "marketId": "<asset_id>" }The server’s trade frame is { type, venue, marketId, data, ts }, where data is a
MarketTrade:
type MarketTrade = { id: string; time: string; timestamp: number; type: 'Buy' | 'Sell'; usd: number; outcome: string; outcomeIndex?: number | null; shares: number; price: number; maker: 'polymarket' | 'kalshi' | 'gemini' | 'limitless' | 'myriad' | 'hyperliquid' | 'sxbet'; transactionHash: string; wallet?: string | null; counterparty?: string | null;};
type TradeFrame = { type: 'trade'; venue: string; marketId: string; data: MarketTrade; ts: number;};This implementation has the three required bar transitions: cold-start from the first
trade, extend the current bucket, or roll into a new bucket whose open is the prior close.
Trade shares accumulate into the forming bar’s volume.
function createLiveBars({ streamUrl, streamReadKey, venue, marketId }) { const sockets = new Map<string, WebSocket>();
function subscribeBars( _symbolInfo, resolution, onRealtimeCallback, subscriberUID, _onResetCacheNeededCallback, ) { const bucketMs = BAR_MS[resolution]; if (!bucketMs) throw new Error(`Unsupported resolution: ${resolution}`);
sockets.get(subscriberUID)?.close(); const socket = new WebSocket(streamUrl); sockets.set(subscriberUID, socket); let currentBar;
socket.addEventListener('open', () => { socket.send(JSON.stringify({ op: 'auth', apiKey: streamReadKey })); socket.send(JSON.stringify({ op: 'subscribe', channel: 'trades', venue, marketId })); });
socket.addEventListener('message', (event) => { let frame: Partial<TradeFrame>; try { frame = JSON.parse(String(event.data)); } catch { return; }
if ( frame.type !== 'trade' || frame.venue !== venue || frame.marketId !== marketId || !frame.data ) { return; }
const { timestamp, price, shares } = frame.data; const bucketStart = Math.floor(timestamp / bucketMs) * bucketMs;
if (!currentBar) { currentBar = { time: bucketStart, open: price, high: price, low: price, close: price, volume: shares, }; } else if (bucketStart === currentBar.time) { currentBar = { ...currentBar, high: Math.max(currentBar.high, price), low: Math.min(currentBar.low, price), close: price, volume: currentBar.volume + shares, }; } else if (bucketStart > currentBar.time) { const open = currentBar.close; currentBar = { time: bucketStart, open, high: Math.max(open, price), low: Math.min(open, price), close: price, volume: shares, }; } else { return; }
onRealtimeCallback({ ...currentBar }); }); }
function unsubscribeBars(subscriberUID) { sockets.get(subscriberUID)?.close(); sockets.delete(subscriberUID); }
return { subscribeBars, unsubscribeBars };}For headerless browser connections, the auth frame must be first and arrive within 10 seconds. Never put the API key in the URL.
Per-venue availability
Section titled “Per-venue availability”Native trades streams are available today for:
| Venue | Live-bar source |
|---|---|
polymarket | Native trades |
kalshi | Native trades |
hyperliquid | Native trades |
sxbet | Native trades |
myriad | Native trades |
Other venues answer a non-fatal NOT_SUPPORTED error and leave the WebSocket open. For
those venues, the fallback pattern is the orderbook channel: turn each full snapshot
or update into a mid-price tick from the best bids and asks. That is a book-derived
mid, not a trade, and it has no trade volume. See the
Streaming capability notes before
choosing that fallback.
A live trade tape for free
Section titled “A live trade tape for free”The same subscription already carries the full MarketTrade. Add another message handler
beside the bar builder to keep the newest N trades for a tape:
const MAX_TRADES = 50;const tape: MarketTrade[] = [];
socket.addEventListener('message', (event) => { const frame = JSON.parse(String(event.data)) as Partial<TradeFrame>; if (frame.type !== 'trade' || !frame.data) return; tape.unshift(frame.data); if (tape.length > MAX_TRADES) tape.length = MAX_TRADES; renderTradeTape(tape);});Polymarket’s native channel does not expose a wallet or transaction hash. Those fields
degrade honestly to wallet: null and transactionHash: ''; prices, shares, side, and
timestamps still drive the bars and tape.