Skip to main content
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

  1. Access to trading view codebase repo (need manual approval from Trading View)
  2. An API key from the Dashboard (only for production use, you can use the API without an API key in development mode)
  3. 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:
FileDescription
datafeed.tsxComplete datafeed with REST + WebSocket OHLCV streaming
index.tsxTradingView chart component with theme support
token/page.tsxToken page with chart integration
pair/page.tsxPair page with chart integration
Live Examples:

Walkthrough

1

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).
2

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>;
};
3

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', '60', '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
  };
};
4

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 });
  }
}
5

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);
  }
}
6

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:
ResolutionPeriodDescription
1s1 secondSub-second precision
5s5 seconds
15s15 seconds
30s30 seconds
1 / 1m1 minute
5 / 5m5 minutes
15 / 15m15 minutes
60 / 1h1 hour
1D / 1d1 day
1W / 1w1 week
1M1 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.