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
Architecture Overview
The MTT Pulse feature uses a modern React architecture:Copy
Ask AI
┌─────────────────────────────────────────────────────────────┐
│ 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:Copy
Ask AI
npx create-next-app@latest my-pulse-terminal --typescript --tailwind --app
cd my-pulse-terminal
Copy
Ask AI
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:Copy
Ask AI
// 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;
}
.env.local:
Copy
Ask AI
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:Copy
Ask AI
// 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:Copy
Ask AI
// 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:Copy
Ask AI
// 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:Copy
Ask AI
// 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:Copy
Ask AI
// 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:Copy
Ask AI
// 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:Copy
Ask AI
// 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:| Filter | Type | Description |
|---|---|---|
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
Copy
Ask AI
// 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
Copy
Ask AI
// 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 usingreact-window:
Copy
Ask AI
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
- Explore the complete MTT source code
- Add TradingView charts for token detail pages
- Implement wallet tracking for live trades
- Build a custom filter modal like the one in MTT