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!

Live Demo

See the Pulse feature in action

Source Code

Explore the complete MTT codebase

Pulse V2 API Docs

Learn about all available filters and data

Get API Key

Sign up for free API access