Documentation Index
Fetch the complete documentation index at: https://docs.mobula.io/llms.txt
Use this file to discover all available pages before exploring further.
Mobula pair data API allows you to fetch real-time and historical price data for any on-chain pair. You can use this data to feed Trading View charting library.
Live Demo: See charts in action on mtt.gg - try any token or pair page!Full Source Code: The complete TradingView implementation is open-source at github.com/MobulaFi/MTT
What you’ll need
- Access to trading view codebase repo (need manual approval from Trading View)
- An API key from the Dashboard (only for production use, you can use the API without an API key in development mode)
- A react app (or any other framework) to display the chart
Open Source Reference Implementation
Before diving into the code, check out the production-ready implementation in the MTT (Mobula Trader Terminal) codebase:
| File | Description |
|---|
datafeed.tsx | Complete datafeed with REST + WebSocket OHLCV streaming |
index.tsx | TradingView chart component with theme support |
token/page.tsx | Token page with chart integration |
pair/page.tsx | Pair page with chart integration |
Live Examples:
Walkthrough
Identify your asset
Pick your asset symbol, name or address. If it is a symbol/name, make sure to check case sensitivity and to respect the asset name as listed on Mobula curated token list (explorable here). If it is an address, make sure to check the blockchain supported (check here for the full list) and to respect the blockchain ID format.You can also use pair address directly (if using asset, we will route to the largest liquidity pool for this asset). Setup Chart Component
Let’s first setup the main chart component - you must host the trading view lib files inside the public folder of your app, in our
example, we host them at static/charting_library.Here’s a simplified version. For the full production implementation with theme support, loading states, and tool persistence, see the MTT chart component.const ChartBox = ({
baseAsset,
mobile = false,
custom_css_url = "../themed.css",
isPair = false,
isUsd = true,
}: ChartBoxProps) => {
const ref = useRef<HTMLDivElement>(null);
const { resolvedTheme } = useTheme();
const isWhiteMode = resolvedTheme === "light";
useEffect(() => {
if (!baseAsset) return () => {};
let freshWidget: IChartingLibraryWidget | null = null;
import("../../../../../../../../public/static/charting_library").then(
({ widget: Widget }) => {
if (!ref.current) return;
// Build symbol for display
const symbol = isPair
? `${baseAsset.base?.symbol ?? baseAsset.symbol}/USD`
: `${baseAsset.symbol}/USD`;
freshWidget = new Widget({
// Datafeed with pair/token mode support
datafeed: Datafeed(baseAsset, isUsd),
symbol,
interval: "60" as ResolutionString,
// Settings
container: ref.current,
container_id: ref.current.id,
library_path: "/static/charting_library/",
// UI & Behavior
locale: "en",
fullscreen: false,
disabled_features: [
"header_saveload",
"header_symbol_search",
...(mobile ? ["left_toolbar"] : []),
],
timezone: Intl.DateTimeFormat().resolvedOptions()
.timeZone as Timezone,
autosize: true,
// Theme
theme: isWhiteMode ? "Light" : "Dark",
overrides: overrides(isWhiteMode),
custom_css_url,
});
}
);
return () => {
freshWidget?.remove();
};
}, [baseAsset, custom_css_url, mobile, isPair, isUsd]);
return <div ref={ref}></div>;
};
Setup the Datafeed with Token & Pair Support
The datafeed handles both token-based and pair-based charts. This is the key difference in the MTT implementation:
- Token mode (
isPair: false): Uses asset parameter to route to the highest liquidity pool
- Pair mode (
isPair: true): Uses address parameter for a specific pool
export const supportedResolutions = ['1s', '5s', '15s', '30s', '1', '5', '15', '30', '60', '240', '1D', '1W', '1M'];
const lastBarsCache = new Map();
const activeSubscriptions = new Map<string, string>();
type BaseAsset = {
asset?: string; // Token address (for token mode)
address?: string; // Pair/pool address (for pair mode)
chainId: string;
symbol?: string;
priceUSD?: number;
isPair?: boolean;
};
export const Datafeed = (initialBaseAsset: BaseAsset, isUsd = false) => {
let baseAsset = initialBaseAsset;
return {
// Allow updating the asset without recreating the datafeed
updateBaseAsset: (newAsset: BaseAsset) => {
baseAsset = newAsset;
},
onReady: (callback: Function) => {
setTimeout(() => {
callback({
supported_resolutions: supportedResolutions,
supports_time: true,
supports_marks: false,
});
}, 0);
},
resolveSymbol: (symbolName: string, onResolve: Function) => {
setTimeout(() => {
const price = baseAsset.priceUSD ?? 1;
onResolve({
name: symbolName,
type: 'crypto',
session: '24x7',
timezone: 'Etc/UTC',
minmov: 1,
pricescale: Math.min(10 ** String(Math.round(10000 / price)).length, 1e16),
has_intraday: true,
has_seconds: true,
supported_resolution: supportedResolutions,
data_status: 'streaming',
});
}, 0);
},
// ... getBars and subscribeBars below
};
};
Fetch Historical Data (getBars)
The getBars function fetches historical OHLCV data. The key is handling both token and pair modes:getBars: async (
_info: any,
resolution: string,
params: any,
onResult: (bars: any[], meta: { noData: boolean }) => void
) => {
const assetId = baseAsset.isPair ? baseAsset.address : baseAsset.asset;
const normalizedResolution = normalizeResolution(resolution);
const cacheKey = `${assetId}-${normalizedResolution}`;
// TradingView provides timestamps in seconds, convert to ms
const fromMs = params.from * 1000;
const toMs = params.to * 1000;
try {
const client = getMobulaClient();
// Build request params based on mode (token vs pair)
const requestParams: any = {
from: fromMs,
to: toMs,
amount: params.countBack,
usd: `${isUsd}`,
period: normalizedResolution,
blockchain: baseAsset.chainId
};
if (baseAsset.isPair) {
// Pair mode: use pool address directly
requestParams.address = baseAsset.address;
requestParams.mode = 'pool';
} else {
// Token mode: use asset address, routes to highest liquidity pool
requestParams.asset = baseAsset.asset;
requestParams.mode = 'asset';
}
const response = await client.fetchMarketHistoricalPairData(requestParams);
const bars = response.data || [];
onResult(bars, { noData: !bars.length });
// Cache last bar for real-time updates
if (bars.length > 0) {
lastBarsCache.set(cacheKey, bars[bars.length - 1]);
}
} catch (err) {
console.error('Error fetching bars:', err);
onResult([], { noData: true });
}
}
Subscribe to Real-Time OHLCV Stream
The MTT implementation uses Mobula’s WebSocket ohlcv stream for real-time candle updates:subscribeBars: async (
_info: any,
resolution: string,
onRealtime: (bar: any) => void,
subscriberUID: string
) => {
const client = getMobulaClient();
if (!client) return;
const assetId = baseAsset.isPair ? baseAsset.address : baseAsset.asset;
const key = `${assetId}-${subscriberUID}`;
const normalizedResolution = normalizeResolution(resolution);
const cacheKey = `${assetId}-${normalizedResolution}`;
// Unsubscribe from existing subscription if any
if (activeSubscriptions.has(key)) {
try {
await client.streams.unsubscribe('ohlcv', activeSubscriptions.get(key)!);
} catch {}
activeSubscriptions.delete(key);
}
try {
// Build subscription params based on mode
const subscribeParams: any = {
period: normalizedResolution,
chainId: baseAsset.chainId,
};
if (baseAsset.isPair) {
subscribeParams.address = baseAsset.address;
} else {
subscribeParams.asset = baseAsset.asset;
}
// Subscribe to OHLCV stream
const subId = client.streams.subscribe(
'ohlcv',
subscribeParams,
(candle: any) => {
if (!candle?.time) return;
// Handle candle timestamp format
const normalizedCandle = {
...candle,
time: candle.time > 10000000000 ? candle.time / 1000 : candle.time
};
onRealtime(normalizedCandle);
lastBarsCache.set(cacheKey, normalizedCandle);
}
);
activeSubscriptions.set(key, subId);
} catch (err) {
console.error('Error subscribing to OHLCV stream', err);
}
},
unsubscribeBars: async (subscriberUID: string) => {
const client = getMobulaClient();
const assetId = baseAsset.isPair ? baseAsset.address : baseAsset.asset;
const key = `${assetId}-${subscriberUID}`;
const subId = activeSubscriptions.get(key);
if (subId && client) {
try {
await client.streams.unsubscribe('ohlcv', subId);
} catch (err) {
console.error('Unsubscribe error:', err);
}
activeSubscriptions.delete(key);
}
}
Resolution Normalization
The MTT datafeed supports sub-second resolutions. Here’s the normalization function:const normalizeResolution = (resolution: string): string => {
switch (resolution) {
case '1S':
case '1s':
return '1s';
case '5S':
case '5s':
return '5s';
case '15S':
case '15s':
return '15s';
case '30S':
case '30s':
return '30s';
case '1':
case '1m':
return '1m';
case '5':
case '5m':
return '5m';
case '15':
case '15m':
return '15m';
case '60':
case '1h':
return '1h';
case '240':
case '4h':
return '4h';
case '1D':
case '1d':
return '1d';
case '1W':
case '1w':
return '1w';
case '1M':
case '1month':
return '1M';
default:
return resolution;
}
};
Token Page vs Pair Page
The MTT terminal has two types of pages that use charts differently:
Token Page (/token/[blockchain]/[address])
Uses the asset mode which automatically routes to the highest liquidity pool:
// Token page passes isPair: false
<TradingViewChart
baseAsset={{
address: tokenData.address, // Token contract address
blockchain: blockchain,
symbol: tokenData.symbol,
priceUSD: tokenData.priceUSD,
}}
isPair={false} // Asset mode
isUsd={true}
/>
Pair Page (/pair/[blockchain]/[address])
Uses the pair mode for a specific pool/pair:
// Pair page passes isPair: true
<TradingViewChart
baseAsset={{
address: pairData.address, // Pool/pair address
blockchain: blockchain,
symbol: pairData.base?.symbol,
priceUSD: pairData.base?.priceUSD,
base: pairData.base,
quote: pairData.quote,
}}
isPair={true} // Pair mode
isUsd={true}
/>
Supported Resolutions
The MTT implementation supports these resolutions:
| Resolution | Period | Description |
|---|
1s | 1 second | Sub-second precision |
5s | 5 seconds | |
15s | 15 seconds | |
30s | 30 seconds | |
1 / 1m | 1 minute | |
5 / 5m | 5 minutes | |
15 / 15m | 15 minutes | |
60 / 1h | 1 hour | |
1D / 1d | 1 day | |
1W / 1w | 1 week | |
1M | 1 month | |
Full Example: Complete Datafeed
For the complete production-ready datafeed with all edge cases handled, see:
src/components/charts/datafeed.tsx
Key features in the production implementation:
- Request deduplication with
pendingRequests map
- Last bar caching for smooth real-time updates
- First candle gap handling (fills gaps between last historical bar and first streaming candle)
- Proper subscription cleanup
- Support for both token and pair modes
Need help?
Can’t find what you’re looking for? Reach out to us, response times < 1h.