Skip to content
Get an API key

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.

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

The REST endpoint is GET {PREDICTEFY_API_URL}/api/{venue}/fetchOHLCV. Your developer dashboard shows the API origin. 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. Send the same pk_live_… key used for streaming as a bearer token.

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({ apiBaseUrl, apiKey, 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({
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(`${apiBaseUrl}/api/${venue}/fetchOHLCV?${params.toString()}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
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.

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:

{ "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, apiKey, 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 }));
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 };
}

The browser handshake must be the first frame and arrive within 10 seconds. Never put the API key in the URL.

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.

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.