Skip to main content
Axiom’s Pulse feature is a powerful token discovery tool that shows real-time data for new tokens, tokens in bonding, and recently migrated tokens. In this guide, you’ll learn how to build a production-ready Pulse feature using Mobula’s open-source MTT (Mobula Trader Terminal) codebase.
Live Demo: Check out the working implementation at mtt.ggSource Code: The complete codebase is available at github.com/MobulaFi/MTT

What You’ll Build

By the end of this guide, you’ll have a Pulse feature with:
  • Three real-time token sections: New Pairs, Final Stretch (bonding), and Migrated
  • WebSocket streaming for instant updates
  • Advanced filtering by chain, protocol, metrics, and audits
  • Performant rendering with batched updates and memoization
  • Persistent filter state using Zustand with localStorage
Pulse Feature Preview

Architecture Overview

The MTT Pulse feature uses a modern React architecture:
┌─────────────────────────────────────────────────────────────┐
│                    PulseStreamProvider                       │
│  (React Context - provides stream state to all children)    │
├─────────────────────────────────────────────────────────────┤
│                       usePulseV2 Hook                        │
│  (WebSocket connection, REST fallback, message batching)    │
├───────────────────────┬─────────────────────────────────────┤
│   usePulseDataStore   │         usePulseFilterStore         │
│   (Token data per     │         (Filter state with          │
│    view: new/bonding/ │          localStorage persist)      │
│    bonded)            │                                     │
├───────────────────────┴─────────────────────────────────────┤
│                      UI Components                           │
│  TokenSection → TokenCard → Filter Modal                    │
└─────────────────────────────────────────────────────────────┘

Step 1: Project Setup

First, create a new Next.js project with the required dependencies:
npx create-next-app@latest my-pulse-terminal --typescript --tailwind --app
cd my-pulse-terminal
Install the necessary packages:
npm install @mobula/sdk zustand immer lucide-react @radix-ui/react-tooltip @radix-ui/react-hover-card
The @mobula/sdk package provides the WebSocket client and REST API methods needed to connect to Mobula’s Pulse V2 stream.

Step 2: Configure the Mobula Client

Create a client configuration file that handles REST and WebSocket connections:
// src/lib/mobulaClient.ts
import { MobulaClient } from '@mobula/sdk';

const REST_ENDPOINTS = {
  PREMIUM: 'https://pulse-v2-api.mobula.io',
  STANDARD: 'https://api.mobula.io',
} as const;

const WSS_REGIONS = {
  default: 'wss://default.mobula.io',
  'pulse-v2': 'wss://pulse-v2-api.mobula.io',
} as const;

let client: MobulaClient | null = null;

export function getMobulaClient(): MobulaClient {
  if (!client) {
    client = new MobulaClient({
      restUrl: REST_ENDPOINTS.PREMIUM,
      apiKey: process.env.NEXT_PUBLIC_MOBULA_API_KEY,
      debug: true,
      timeout: 200000,
    });
  }
  return client;
}
Add your API key to .env.local:
NEXT_PUBLIC_MOBULA_API_KEY=your_api_key_here
Get your free API key at admin.mobula.io

Step 3: Create the Data Store

The data store manages tokens for each view (new, bonding, bonded) with optimized sorting and token limits:
// src/store/usePulseDataStore.ts
'use client';

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

export type ViewName = 'new' | 'bonding' | 'bonded';

export interface PulseToken {
  address?: string;
  chainId?: string;
  name?: string;
  symbol?: string;
  logo?: string;
  marketCap?: number;
  bondingPercentage?: number;
  holders_count?: number;
  price_change_24h?: number;
  created_at?: string;
  bonded_at?: string;
  [key: string]: unknown;
}

export interface SectionDataState {
  tokens: PulseToken[];
  loading: boolean;
  error: string | null;
  lastUpdate: number;
  searchQuery: string;
}

const TOKEN_LIMIT = 50;

function getTokenKey(token: PulseToken): string {
  return `${token.address || ''}_${token.chainId || ''}`;
}

function getTokenTimestamp(token: PulseToken, view: ViewName): number {
  const timestampStr = view === 'bonded' 
    ? token.bonded_at || token.created_at 
    : token.created_at;
  
  if (!timestampStr) return 0;
  const timestamp = new Date(timestampStr).getTime();
  return Number.isNaN(timestamp) ? 0 : timestamp;
}

export interface PulseDataStoreState {
  sections: Record<ViewName, SectionDataState>;
  setTokens(view: ViewName, tokens: PulseToken[]): void;
  setLoading(view: ViewName, loading: boolean): void;
  setError(view: ViewName, error: string | null): void;
  mergeToken(view: ViewName, token: PulseToken): void;
  clearView(view: ViewName): void;
  setSearchQuery(view: ViewName, query: string): void;
}

export const usePulseDataStore = create<PulseDataStoreState>()(
  devtools(
    immer((set) => ({
      sections: {
        new: { tokens: [], loading: false, error: null, lastUpdate: 0, searchQuery: '' },
        bonding: { tokens: [], loading: false, error: null, lastUpdate: 0, searchQuery: '' },
        bonded: { tokens: [], loading: false, error: null, lastUpdate: 0, searchQuery: '' },
      },

      setTokens(view, tokens) {
        set((state) => {
          // Sort by timestamp (newest first)
          const sortedTokens = [...tokens].sort((a, b) => {
            return getTokenTimestamp(b, view) - getTokenTimestamp(a, view);
          });
          
          // Enforce token limit
          const limitedTokens = sortedTokens.slice(0, TOKEN_LIMIT);
          
          state.sections[view].tokens = limitedTokens;
          state.sections[view].lastUpdate = Date.now();
          state.sections[view].error = null;
        });
      },

      setLoading(view, loading) {
        set((state) => {
          state.sections[view].loading = loading;
        });
      },

      setError(view, error) {
        set((state) => {
          state.sections[view].error = error;
          state.sections[view].loading = false;
        });
      },

      mergeToken(view, token) {
        set((state) => {
          const currentTokens = state.sections[view].tokens;
          const tokenKey = getTokenKey(token);
          const existingIndex = currentTokens.findIndex((t) => getTokenKey(t) === tokenKey);

          if (existingIndex !== -1) {
            // Update existing token
            Object.assign(currentTokens[existingIndex], token);
          } else {
            // Add new token at the beginning (newest first)
            currentTokens.unshift(token);
            
            // Enforce limit
            if (currentTokens.length > TOKEN_LIMIT) {
              currentTokens.splice(TOKEN_LIMIT);
            }
          }
          
          state.sections[view].lastUpdate = Date.now();
        });
      },

      clearView(view) {
        set((state) => {
          state.sections[view].tokens = [];
          state.sections[view].loading = false;
          state.sections[view].error = null;
          state.sections[view].searchQuery = '';
        });
      },

      setSearchQuery(view, query) {
        set((state) => {
          state.sections[view].searchQuery = query;
        });
      },
    })),
    { name: 'PulseDataStore' }
  )
);

Step 4: Create the Pulse V2 Hook

This is the core hook that manages WebSocket subscriptions and handles real-time updates:
// src/hooks/usePulseV2.ts
'use client';

import { useEffect, useRef, useMemo, useState, useCallback } from 'react';
import { getMobulaClient } from '@/lib/mobulaClient';
import type { MobulaClient } from '@mobula/sdk';
import { usePulseDataStore, type ViewName, type PulseToken } from '@/store/usePulseDataStore';

interface PulseViewData {
  data?: PulseToken | PulseToken[];
}

type WssPulseV2ResponseType =
  | {
      type: 'init';
      payload: {
        new?: PulseViewData;
        bonding?: PulseViewData;
        bonded?: PulseViewData;
      };
    }
  | {
      type: 'update-token';
      payload: {
        viewName: string;
        token: PulseToken;
      };
    };

export interface UsePulseV2Options {
  enabled?: boolean;
  chainIds?: string[];
  filters?: Record<string, unknown>;
}

export function usePulseV2({
  enabled = true,
  chainIds = ['solana:solana'],
  filters = {},
}: UsePulseV2Options = {}) {
  const clientRef = useRef<MobulaClient | null>(null);
  const subscriptionIdRef = useRef<string | null>(null);
  
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  
  const pulseDataStore = usePulseDataStore();

  // Build subscription payload
  const payload = useMemo(() => {
    const buildView = (name: string, model: 'new' | 'bonding' | 'bonded') => ({
      name,
      chainId: chainIds,
      limit: 50,
      model,
      ...(Object.keys(filters).length > 0 && { filters }),
    });

    return {
      assetMode: true,
      compressed: false,
      views: [
        buildView('new', 'new'),
        buildView('bonding', 'bonding'),
        buildView('bonded', 'bonded'),
      ],
    };
  }, [chainIds, filters]);

  // Initialize client
  const getClient = useCallback(() => {
    if (!clientRef.current) {
      clientRef.current = getMobulaClient();
    }
    return clientRef.current;
  }, []);

  // Handle incoming WebSocket messages
  const handleMessage = useCallback((msg: WssPulseV2ResponseType) => {
    if (isPaused || !msg) return;

    if (msg.type === 'init') {
      const { new: newData, bonding: bondingData, bonded: bondedData } = msg.payload;
      
      if (newData?.data) {
        const tokens = Array.isArray(newData.data) ? newData.data : [newData.data];
        pulseDataStore.setTokens('new', tokens);
      }
      if (bondingData?.data) {
        const tokens = Array.isArray(bondingData.data) ? bondingData.data : [bondingData.data];
        pulseDataStore.setTokens('bonding', tokens);
      }
      if (bondedData?.data) {
        const tokens = Array.isArray(bondedData.data) ? bondedData.data : [bondedData.data];
        pulseDataStore.setTokens('bonded', tokens);
      }
      
      setLoading(false);
    }

    if (msg.type === 'update-token') {
      const viewName = msg.payload.viewName as ViewName;
      const token = msg.payload.token;
      
      if (viewName && token) {
        pulseDataStore.mergeToken(viewName, token);
      }
    }
  }, [isPaused, pulseDataStore]);

  // Load initial data via REST API (faster first paint)
  const loadInitialData = useCallback(async () => {
    try {
      const client = getClient();
      const restResponse = await client.fetchPulseV2({
        model: 'default',
        assetMode: true,
        compressed: false,
        chainId: chainIds,
      });

      if (restResponse.new?.data) {
        pulseDataStore.setTokens('new', 
          Array.isArray(restResponse.new.data) ? restResponse.new.data : [restResponse.new.data]
        );
      }
      if (restResponse.bonding?.data) {
        pulseDataStore.setTokens('bonding', 
          Array.isArray(restResponse.bonding.data) ? restResponse.bonding.data : [restResponse.bonding.data]
        );
      }
      if (restResponse.bonded?.data) {
        pulseDataStore.setTokens('bonded', 
          Array.isArray(restResponse.bonded.data) ? restResponse.bonded.data : [restResponse.bonded.data]
        );
      }
      
      setLoading(false);
    } catch (e) {
      console.error('[usePulseV2] REST load failed:', e);
    }
  }, [getClient, chainIds, pulseDataStore]);

  // Subscribe to WebSocket stream
  const subscribe = useCallback(() => {
    try {
      const client = getClient();
      
      subscriptionIdRef.current = client.streams.subscribe(
        'pulse-v2',
        payload,
        (data: unknown) => {
          handleMessage(data as WssPulseV2ResponseType);
        }
      );
      
      setIsConnected(true);
      setError(null);
    } catch (e) {
      console.error('[usePulseV2] Subscribe error:', e);
      setError(e instanceof Error ? e.message : 'Subscribe failed');
    }
  }, [getClient, payload, handleMessage]);

  // Unsubscribe from WebSocket
  const unsubscribe = useCallback(() => {
    if (subscriptionIdRef.current && clientRef.current?.streams) {
      try {
        clientRef.current.streams.unsubscribe('pulse-v2', subscriptionIdRef.current);
      } catch (e) {
        console.warn('[usePulseV2] Unsubscribe error:', e);
      }
    }
    subscriptionIdRef.current = null;
    setIsConnected(false);
  }, []);

  // Pause/Resume controls
  const pauseSubscription = useCallback(() => setIsPaused(true), []);
  const resumeSubscription = useCallback(() => setIsPaused(false), []);

  // Initial load effect
  useEffect(() => {
    if (!enabled) return;
    
    loadInitialData();
    
    const timer = setTimeout(() => {
      subscribe();
    }, 500);

    return () => {
      clearTimeout(timer);
      unsubscribe();
    };
  }, [enabled, loadInitialData, subscribe, unsubscribe]);

  return {
    loading,
    error,
    isConnected,
    isPaused,
    pauseSubscription,
    resumeSubscription,
  };
}

Step 5: Create the Stream Context

Wrap your components with a context provider to share stream state:
// src/context/PulseStreamContext.tsx
'use client';

import { createContext, useContext, type ReactNode } from 'react';
import { usePulseV2 } from '@/hooks/usePulseV2';

type PulseStreamContextValue = ReturnType<typeof usePulseV2>;

const PulseStreamContext = createContext<PulseStreamContextValue | null>(null);

export function PulseStreamProvider({ children }: { children: ReactNode }) {
  const pulseStream = usePulseV2({ enabled: true });

  return (
    <PulseStreamContext.Provider value={pulseStream}>
      {children}
    </PulseStreamContext.Provider>
  );
}

export function usePulseStreamContext() {
  const context = useContext(PulseStreamContext);
  if (!context) {
    throw new Error('usePulseStreamContext must be used inside PulseStreamProvider');
  }
  return context;
}

Step 6: Build the Token Card Component

Create a reusable card component to display token information:
// src/components/TokenCard.tsx
'use client';

import { memo, useMemo } from 'react';
import Image from 'next/image';
import type { PulseToken } from '@/store/usePulseDataStore';

interface TokenCardProps {
  token: PulseToken;
  viewName: 'new' | 'bonding' | 'bonded';
}

function formatPrice(value: number): string {
  if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M`;
  if (value >= 1_000) return `$${(value / 1_000).toFixed(2)}K`;
  return `$${value.toFixed(2)}`;
}

function formatPercentage(value: number): string {
  const sign = value >= 0 ? '+' : '';
  return `${sign}${value.toFixed(2)}%`;
}

function formatTimeAgo(timestamp: string): string {
  const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
  if (seconds < 60) return `${seconds}s ago`;
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
  return `${Math.floor(seconds / 86400)}d ago`;
}

function TokenCard({ token, viewName }: TokenCardProps) {
  const priceChange = token.price_change_24h ?? 0;
  const timestamp = viewName === 'bonded' && token.bonded_at 
    ? token.bonded_at 
    : token.created_at;

  const bondingLabel = useMemo(() => {
    if (viewName === 'bonded') return 'Migrated';
    return `${(token.bondingPercentage ?? 0).toFixed(1)}%`;
  }, [viewName, token.bondingPercentage]);

  return (
    <div className="bg-[#121319] hover:bg-[#1D2028] transition-all duration-200 px-3 py-2 rounded-md cursor-pointer">
      <div className="flex justify-between items-start gap-4">
        {/* Token Info */}
        <div className="flex space-x-3 flex-1 min-w-0">
          {/* Logo */}
          <div className="flex-shrink-0 w-12 h-12 relative">
            {token.logo ? (
              <Image
                src={token.logo}
                alt={token.symbol || 'Token'}
                width={48}
                height={48}
                className="rounded-full object-cover"
              />
            ) : (
              <div className="w-full h-full bg-gray-700 rounded-full flex items-center justify-center">
                <span className="text-xs font-bold text-gray-300">
                  {(token.symbol || '?')[0].toUpperCase()}
                </span>
              </div>
            )}
          </div>

          {/* Details */}
          <div className="flex flex-col space-y-1 min-w-0">
            <div className="flex items-center gap-2">
              <span className="font-semibold text-white truncate">
                {token.name || 'Unknown'}
              </span>
              <span className="text-xs text-gray-400 uppercase">
                {token.symbol}
              </span>
            </div>
            
            <div className="flex items-center gap-2 text-xs text-gray-500">
              <span>{timestamp ? formatTimeAgo(timestamp) : '-'}</span>
              <span className="text-purple-400 font-mono">
                {token.address?.slice(0, 6)}...{token.address?.slice(-4)}
              </span>
            </div>

            {/* Stats */}
            <div className="flex items-center gap-3 text-xs">
              <span className="text-gray-400">
                👥 {token.holders_count ?? 0}
              </span>
              <span className={`px-1.5 py-0.5 rounded ${
                viewName === 'bonded' ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'
              }`}>
                {bondingLabel}
              </span>
            </div>
          </div>
        </div>

        {/* Market Data */}
        <div className="flex flex-col items-end flex-shrink-0 min-w-[80px]">
          <div className="flex items-center gap-1 text-xs">
            <span className="text-gray-400">MCap</span>
            <span className="text-white font-semibold">
              {formatPrice(token.marketCap ?? 0)}
            </span>
          </div>
          <div className={`text-xs font-medium ${
            priceChange >= 0 ? 'text-green-400' : 'text-red-400'
          }`}>
            {formatPercentage(priceChange)}
          </div>
        </div>
      </div>
    </div>
  );
}

export default memo(TokenCard);

Step 7: Build the Token Section Component

Create a section component that displays a list of tokens with real-time status:
// src/components/TokenSection.tsx
'use client';

import { useMemo } from 'react';
import Link from 'next/link';
import TokenCard from './TokenCard';
import { usePulseDataStore, type ViewName } from '@/store/usePulseDataStore';
import { usePulseStreamContext } from '@/context/PulseStreamContext';

interface TokenSectionProps {
  title: string;
  viewName: ViewName;
}

export default function TokenSection({ title, viewName }: TokenSectionProps) {
  const tokens = usePulseDataStore((state) => state.sections[viewName].tokens);
  const searchQuery = usePulseDataStore((state) => state.sections[viewName].searchQuery);
  const { loading, isConnected, isPaused } = usePulseStreamContext();

  // Filter tokens by search query
  const filteredTokens = useMemo(() => {
    if (!searchQuery.trim()) return tokens;
    const query = searchQuery.toLowerCase();
    return tokens.filter((token) => {
      const name = (token.name || '').toLowerCase();
      const symbol = (token.symbol || '').toLowerCase();
      const address = (token.address || '').toLowerCase();
      return name.includes(query) || symbol.includes(query) || address.includes(query);
    });
  }, [tokens, searchQuery]);

  // Status badge
  const statusBadge = useMemo(() => {
    if (loading) return { text: 'LOADING', color: 'bg-yellow-500/20 text-yellow-400' };
    if (isPaused) return { text: 'PAUSED', color: 'bg-yellow-500/20 text-yellow-400' };
    if (isConnected) return { text: 'LIVE', color: 'bg-green-500/20 text-green-400' };
    return { text: 'OFFLINE', color: 'bg-gray-500/20 text-gray-400' };
  }, [loading, isConnected, isPaused]);

  return (
    <div className="bg-[#121319] max-h-[calc(100vh-200px)] overflow-hidden overflow-y-auto">
      {/* Header */}
      <div className="sticky top-0 z-10 bg-[#0F1116] flex justify-between items-center px-3 py-2 border-b border-gray-700">
        <div className="flex items-center gap-2">
          <h2 className="text-sm font-medium text-white">{title}</h2>
          <span className="text-xs text-gray-500">({filteredTokens.length})</span>
          
          {/* Live Status Indicator */}
          <div className="flex items-center gap-1.5">
            <div className={`w-2 h-2 rounded-full ${
              isConnected && !isPaused ? 'bg-green-500 animate-pulse' : 'bg-gray-500'
            }`} />
            <span className={`text-xs font-medium px-1.5 py-0.5 rounded ${statusBadge.color}`}>
              {statusBadge.text}
            </span>
          </div>
        </div>
      </div>

      {/* Token List */}
      <div className="space-y-0">
        {loading ? (
          // Loading skeleton
          <div className="space-y-2 p-3">
            {[...Array(5)].map((_, i) => (
              <div key={i} className="animate-pulse bg-gray-800 h-16 rounded-md" />
            ))}
          </div>
        ) : filteredTokens.length === 0 ? (
          <div className="p-4 text-gray-500 text-xs text-center">
            {searchQuery ? `No tokens match "${searchQuery}"` : 'No tokens found'}
          </div>
        ) : (
          filteredTokens.map((token) => (
            <Link
              key={`${token.address}-${token.chainId}`}
              href={`/token/${token.chainId}/${token.address}`}
              className="block border-b border-gray-800"
            >
              <TokenCard token={token} viewName={viewName} />
            </Link>
          ))
        )}
      </div>
    </div>
  );
}

Step 8: Assemble the Main Page

Put everything together on the main page:
// src/app/page.tsx
'use client';

import TokenSection from '@/components/TokenSection';
import { PulseStreamProvider, usePulseStreamContext } from '@/context/PulseStreamContext';

function PulsePageContent() {
  const { error } = usePulseStreamContext();

  if (error) {
    return (
      <div className="p-4 text-red-500">
        Error loading pulse data: {error}
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-[#121319]">
      {/* Header */}
      <div className="px-4 py-3">
        <h1 className="text-xl font-bold text-white">Pulse</h1>
        <p className="text-sm text-gray-400">Real-time token discovery</p>
      </div>

      {/* Three Column Layout */}
      <div className="px-4 grid md:grid-cols-3 gap-0 border-t border-gray-700">
        {/* New Pairs */}
        <div className="border-r border-gray-700">
          <TokenSection title="New Pairs" viewName="new" />
        </div>

        {/* Final Stretch (Bonding) */}
        <div className="border-r border-gray-700">
          <TokenSection title="Final Stretch" viewName="bonding" />
        </div>

        {/* Migrated */}
        <div>
          <TokenSection title="Migrated" viewName="bonded" />
        </div>
      </div>
    </div>
  );
}

export default function HomePage() {
  return (
    <PulseStreamProvider>
      <PulsePageContent />
    </PulseStreamProvider>
  );
}

Step 9: Add Advanced Filtering

The MTT codebase includes comprehensive filtering capabilities. Here’s how to add filter support:
// src/store/usePulseFilterStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

export interface FilterState {
  chainIds: string[];
  protocols: string[];
  metrics: {
    volume: { min: string; max: string };
    marketCap: { min: string; max: string };
    liquidity: { min: string; max: string };
  };
  audits: {
    holders: { min: string; max: string };
    top10HoldersPercent: { min: string; max: string };
    devHoldingPercent: { min: string; max: string };
  };
}

const defaultFilters: FilterState = {
  chainIds: ['solana:solana'],
  protocols: [],
  metrics: {
    volume: { min: '', max: '' },
    marketCap: { min: '', max: '' },
    liquidity: { min: '', max: '' },
  },
  audits: {
    holders: { min: '', max: '' },
    top10HoldersPercent: { min: '', max: '' },
    devHoldingPercent: { min: '', max: '' },
  },
};

interface PulseFilterStore {
  filters: FilterState;
  setFilter<K extends keyof FilterState>(key: K, value: FilterState[K]): void;
  resetFilters(): void;
  buildApiFilters(): Record<string, unknown>;
}

export const usePulseFilterStore = create<PulseFilterStore>()(
  persist(
    (set, get) => ({
      filters: defaultFilters,

      setFilter(key, value) {
        set((state) => ({
          filters: { ...state.filters, [key]: value },
        }));
      },

      resetFilters() {
        set({ filters: defaultFilters });
      },

      buildApiFilters() {
        const { filters } = get();
        const apiFilters: Record<string, unknown> = {};

        // Volume filter
        if (filters.metrics.volume.min || filters.metrics.volume.max) {
          apiFilters.volume_24h = {};
          if (filters.metrics.volume.min) {
            (apiFilters.volume_24h as Record<string, number>).gte = Number(filters.metrics.volume.min);
          }
          if (filters.metrics.volume.max) {
            (apiFilters.volume_24h as Record<string, number>).lte = Number(filters.metrics.volume.max);
          }
        }

        // Market cap filter
        if (filters.metrics.marketCap.min || filters.metrics.marketCap.max) {
          apiFilters.market_cap = {};
          if (filters.metrics.marketCap.min) {
            (apiFilters.market_cap as Record<string, number>).gte = Number(filters.metrics.marketCap.min);
          }
          if (filters.metrics.marketCap.max) {
            (apiFilters.market_cap as Record<string, number>).lte = Number(filters.metrics.marketCap.max);
          }
        }

        // Holders filter
        if (filters.audits.holders.min || filters.audits.holders.max) {
          apiFilters.holders_count = {};
          if (filters.audits.holders.min) {
            (apiFilters.holders_count as Record<string, number>).gte = Number(filters.audits.holders.min);
          }
          if (filters.audits.holders.max) {
            (apiFilters.holders_count as Record<string, number>).lte = Number(filters.audits.holders.max);
          }
        }

        return apiFilters;
      },
    }),
    {
      name: 'pulse-filters',
      storage: createJSONStorage(() => localStorage),
    }
  )
);

Available Filter Parameters

The Pulse V2 API supports extensive filtering options. Here are the key filter parameters:
FilterTypeDescription
volume_24h{ gte?, lte? }24-hour trading volume
market_cap{ gte?, lte? }Market capitalization
liquidity{ gte?, lte? }Available liquidity
holders_count{ gte?, lte? }Number of token holders
top10_holders_percent{ gte?, lte? }Top 10 holders percentage
dev_holdings_percentage{ gte?, lte? }Developer holdings percentage
snipers_holdings_percentage{ gte?, lte? }Sniper wallets holdings
insiders_holdings_percentage{ gte?, lte? }Insider holdings percentage
bonding_percentage{ gte?, lte? }Bonding curve progress
pro_traders_count{ gte?, lte? }Number of pro traders
dexscreener_ad_paid{ equals: true }DEX Screener ad status
For the complete list of filter parameters, see the Pulse Stream V2 documentation.

Performance Optimizations

The MTT codebase uses several techniques for optimal performance:

1. Update Batching

// Batch multiple token updates using requestAnimationFrame
class UpdateBatcher<T> {
  private updates: T[] = [];
  private scheduled = false;
  
  constructor(private callback: (updates: T[]) => void) {}
  
  add(update: T) {
    this.updates.push(update);
    if (!this.scheduled) {
      this.scheduled = true;
      requestAnimationFrame(() => {
        this.callback(this.updates);
        this.updates = [];
        this.scheduled = false;
      });
    }
  }
}

2. Memoized Components

// Use React.memo with custom comparison
export default memo(TokenCard, (prev, next) => {
  return (
    prev.token.price_change_24h === next.token.price_change_24h &&
    prev.token.marketCap === next.token.marketCap &&
    prev.token.holders_count === next.token.holders_count
  );
});

3. Virtualized Lists

For large token lists, consider using react-window:
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={600}
  width="100%"
  itemCount={tokens.length}
  itemSize={80}
>
  {({ index, style }) => (
    <div style={style}>
      <TokenCard token={tokens[index]} viewName={viewName} />
    </div>
  )}
</FixedSizeList>

Conclusion

You now have a fully functional Axiom-style Pulse feature built with:
  • Real-time WebSocket streaming for instant token updates
  • REST API fallback for fast initial page loads
  • Zustand state management with localStorage persistence
  • Performant rendering with batched updates and memoization
  • Advanced filtering by metrics, audits, and holder data

Next Steps


Get Started

Create a free Mobula API key and start building your own crypto terminal today!