Mobula’s Pulse V2 WebSocket stream delivers real-time token data — new pairs, bonding curve progress, and migrations — directly to your application. This guide shows you how to integrate the Pulse WSS using the @mobula/sdk, with practical patterns taken from the open-source MTT (Mobula Trader Terminal) codebase.
Prerequisites
Node.js 18+ or Bun
A Mobula API key (Growth or Enterprise plan) — get one at admin.mobula.io
@mobula/sdk installed in your project
npm install @mobula/sdk
# or
bun add @mobula/sdk
Quick Start
Connect to Pulse V2 and start receiving real-time token data in under 10 lines:
import { MobulaClient } from '@mobula/sdk' ;
const client = new MobulaClient ({
restUrl: 'https://pulse-v2-api.mobula.io' ,
apiKey: 'YOUR_API_KEY' ,
});
// Subscribe to the pulse-v2 stream
const subscriptionId = client . streams . subscribe (
'pulse-v2' ,
{
assetMode: true ,
views: [
{ name: 'new' , model: 'new' , chainId: [ 'solana:solana' ], limit: 30 },
{ name: 'bonding' , model: 'bonding' , chainId: [ 'solana:solana' ], limit: 30 },
{ name: 'bonded' , model: 'bonded' , chainId: [ 'solana:solana' ], limit: 30 },
],
},
( data ) => {
console . log ( 'Received:' , data );
}
);
That’s it — you’re now receiving live token updates.
How the SDK StreamClient Works
The MobulaClient exposes a streams property — an instance of StreamClient that manages WebSocket connections. Key behaviors:
Feature Detail Auto-reconnect Exponential backoff up to 30 seconds Heartbeat Pings every 30s to keep connections alive Deduplication Subscriptions are hashed to prevent duplicates Compression Per-message deflate supported Queue Messages queued until the WebSocket is ready
Core Methods
// Subscribe — returns a subscription ID
const id = client . streams . subscribe ( 'pulse-v2' , payload , callback );
// Unsubscribe by ID
await client . streams . unsubscribe ( 'pulse-v2' , id );
// Close the WebSocket connection entirely
client . streams . close ( 'pulse-v2' );
Understanding the Payload
The subscription payload controls what data you receive. Here’s the full shape:
interface PulsePayload {
assetMode ?: boolean ; // true = token mode, false = pool mode
compressed ?: boolean ; // gzip-compressed responses
views : Array <{
name : string ; // Your label for this view
model : 'new' | 'bonding' | 'bonded' ; // Token lifecycle stage
chainId ?: string []; // e.g. ['solana:solana', 'evm:56']
poolTypes ?: string []; // Filter by pool type
sortBy ?: string ; // Sort field
sortOrder ?: 'asc' | 'desc' ;
limit ?: number ; // Max tokens per view (max 100, default 30)
offset ?: number ; // Pagination offset
filters ?: Record < string , unknown >; // Advanced filters (see below)
min_socials ?: number ; // 1-3, require social links
}>;
}
View Models
Model Description newFreshly created tokens still in their bonding phase bondingTokens nearing the end of their bonding curve (final stretch) bondedTokens that have migrated to a DEX (graduated)
Message Types
The stream sends three message types:
init — Initial Data Snapshot
Sent immediately after subscribing. Contains the current state for all views:
{
type : 'init' ,
payload : {
new : { data : PulseToken [] },
bonding : { data : PulseToken [] },
bonded : { data : PulseToken [] },
}
}
update-token — Token Data Changed
Sent when a token’s metrics update (price, holders, market cap, etc.):
{
type : 'update-token' ,
payload : {
viewName : 'new' | 'bonding' | 'bonded' ,
token : PulseToken ,
}
}
new-token — New Token Appeared
Sent when a token enters a view for the first time:
{
type : 'new-token' ,
payload : {
viewName : 'new' | 'bonding' | 'bonded' ,
token : PulseToken ,
}
}
REST + WebSocket Pattern
MTT uses a REST-first, WebSocket-second pattern for the best user experience: load data instantly via REST, then switch to WebSocket for live updates.
Here’s a simplified version of how MTT does it (apps/mtt/src/features/pulse/hooks/usePulseV2.ts):
import { MobulaClient } from '@mobula/sdk' ;
const client = new MobulaClient ({
restUrl: 'https://pulse-v2-api.mobula.io' ,
apiKey: 'YOUR_API_KEY' ,
});
// 1. Load initial data via REST (fast first paint)
const initialData = await client . fetchPulseV2 ({
assetMode: true ,
compressed: false ,
views: [
{ name: 'new' , model: 'new' , chainId: [ 'solana:solana' ], limit: 50 },
{ name: 'bonding' , model: 'bonding' , chainId: [ 'solana:solana' ], limit: 50 },
{ name: 'bonded' , model: 'bonded' , chainId: [ 'solana:solana' ], limit: 50 },
],
});
// Render initialData immediately...
// 2. Then subscribe to WebSocket for live updates
const subscriptionId = client . streams . subscribe (
'pulse-v2' ,
{
assetMode: true ,
views: [
{ name: 'new' , model: 'new' , chainId: [ 'solana:solana' ], limit: 50 },
{ name: 'bonding' , model: 'bonding' , chainId: [ 'solana:solana' ], limit: 50 },
{ name: 'bonded' , model: 'bonded' , chainId: [ 'solana:solana' ], limit: 50 },
],
},
( msg ) => {
if ( msg . type === 'init' ) {
// Replace all view data
}
if ( msg . type === 'update-token' || msg . type === 'new-token' ) {
// Merge single token into the right view
}
}
);
The REST and WebSocket APIs accept the same payload shape, so you can reuse your view configuration for both.
How MTT Implements Pulse WSS
MTT is Mobula’s open-source trading terminal. Its Pulse implementation is a production reference you can study and adapt. Here are the key architectural pieces:
Architecture
┌──────────────────────────────────────────────────────┐
│ PulseStreamProvider │
│ React Context — shares stream state with all children│
├──────────────────────────────────────────────────────┤
│ usePulseV2 Hook │
│ REST initial load → WebSocket subscription │
│ Message batching → Pause/Resume → Filter resubscribe │
├─────────────────────┬────────────────────────────────┤
│ usePulseDataStore │ usePulseFilterStore │
│ Zustand store: │ Zustand + localStorage: │
│ tokens per view │ chain, protocol, metric │
│ merge / sort / cap │ and audit filters │
├─────────────────────┴────────────────────────────────┤
│ UI Components │
│ TokenSection → TokenCard → FilterModal │
└──────────────────────────────────────────────────────┘
Key Files in the MTT Codebase
File Purpose src/lib/sdkClient.tsSDK wrapper with server/client mode routing src/features/pulse/hooks/usePulseV2.tsCore hook — REST load, WSS subscribe, message handling src/features/pulse/store/usePulseDataStore.tsToken state management per view src/features/pulse/store/usePulseModalFilterStore.tsFilter state with localStorage persistence src/features/pulse/context/PulseStreamContext.tsxReact context provider for stream state src/features/pulse/components/FilterModal.tsxFilter UI that triggers resubscription src/config/endpoints.tsWSS and REST URL configuration
SDK Client Wrapper
MTT wraps the SDK to support both server-side (SSR) and client-side modes. Here’s the stream subscription pattern from sdkClient.ts:
import { MobulaClient } from '@mobula/sdk' ;
import type { PulsePayloadParams } from '@mobula/types' ;
type StreamSubscription = { unsubscribe : () => void };
function subscribePulseV2 (
params : PulsePayloadParams ,
callback : ( data : unknown ) => void
) : StreamSubscription {
const client = getMobulaClient (); // your singleton
const subscriptionId = client . streams . subscribe ( 'pulse-v2' , params , callback );
return {
unsubscribe : () => {
client . streams . unsubscribe ( 'pulse-v2' , subscriptionId );
},
};
}
Update Batching for 60fps
MTT batches incoming WebSocket messages using requestAnimationFrame to avoid render storms:
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 ;
});
}
}
}
// Usage in the pulse hook
const batcher = new UpdateBatcher <{ view : string ; token : PulseToken }>(( updates ) => {
updates . forEach (({ view , token }) => {
dataStore . mergeToken ( view , token );
});
});
// In the WebSocket callback:
client . streams . subscribe ( 'pulse-v2' , payload , ( msg ) => {
if ( msg . type === 'update-token' || msg . type === 'new-token' ) {
batcher . add ({ view: msg . payload . viewName , token: msg . payload . token });
}
});
Without batching, high-frequency token updates can cause hundreds of React re-renders per second. The UpdateBatcher coalesces them into a single render per animation frame.
Pause and Resume
The SDK supports pausing specific views without disconnecting:
// Pause views (stop receiving updates for these views)
client . streams . subscribe ( 'pulse-v2-pause' , {
action: 'pause' ,
views: [ 'new' , 'bonding' ],
});
// Resume views
client . streams . subscribe ( 'pulse-v2-pause' , {
action: 'unpause' ,
views: [ 'new' , 'bonding' ],
});
MTT uses this when the user scrolls away from a section or when applying filters (to prevent flickering during resubscription):
// From usePulseV2.ts — pause during filter changes
const applyFilters = () => {
setIsPaused ( true );
// Unsubscribe old → resubscribe with new filters
// Resume after a short delay
setTimeout (() => setIsPaused ( false ), 500 );
};
Filtering
Pass filters inside each view to narrow down the token stream. Filters use { gte, lte } range syntax:
client . streams . subscribe ( 'pulse-v2' , {
assetMode: true ,
views: [
{
name: 'new' ,
model: 'new' ,
chainId: [ 'solana:solana' ],
limit: 50 ,
filters: {
market_cap: { gte: 10000 , lte: 1000000 },
holders_count: { gte: 50 },
volume_24h: { gte: 5000 },
dev_holdings_percentage: { lte: 10 },
top10_holders_percent: { lte: 50 },
},
},
],
});
Available Filter Fields
Filter Type Description market_cap{ gte?, lte? }Market capitalization in USD volume_24h{ gte?, lte? }24-hour trading volume liquidity{ gte?, lte? }Available liquidity holders_count{ gte?, lte? }Number of token holders top10_holders_percent{ gte?, lte? }Top 10 holders concentration dev_holdings_percentage{ gte?, lte? }Developer holdings % snipers_holdings_percentage{ gte?, lte? }Sniper wallet holdings % insiders_holdings_percentage{ gte?, lte? }Insider holdings % bundlers_holdings_percentage{ gte?, lte? }Bundler holdings % bonding_percentage{ gte?, lte? }Bonding curve progress % pro_traders_count{ gte?, lte? }Number of pro traders trades_24h{ gte?, lte? }24h transaction count buys_24h{ gte?, lte? }24h buy count sells_24h{ gte?, lte? }24h sell count fees_paid_24h{ gte?, lte? }Fees paid in 24h created_at_offset{ gte?, lte? }Token age in seconds deployer_migrations_count{ gte?, lte? }Deployer’s migration count dexscreener_ad_paid{ equals: true }Has DEX Screener ad twitter{ not: null }Has Twitter link website{ not: null }Has website telegram{ not: null }Has Telegram link min_socialsnumberMin social links (1-3) includeKeywordsstring[]Token name must contain one of these excludeKeywordsstring[]Token name must not contain these
Multi-Chain Support
Subscribe to multiple chains in a single view:
client . streams . subscribe ( 'pulse-v2' , {
assetMode: true ,
views: [
{
name: 'new-multichain' ,
model: 'new' ,
chainId: [ 'solana:solana' , 'evm:56' , 'evm:8453' , 'evm:1' ],
limit: 50 ,
},
],
});
Or use separate views per chain for independent control:
client . streams . subscribe ( 'pulse-v2' , {
assetMode: true ,
views: [
{ name: 'new-sol' , model: 'new' , chainId: [ 'solana:solana' ], limit: 30 },
{ name: 'new-bsc' , model: 'new' , chainId: [ 'evm:56' ], limit: 30 },
{ name: 'new-base' , model: 'new' , chainId: [ 'evm:8453' ], limit: 30 },
],
});
Custom WSS URLs
The SDK lets you override WebSocket endpoints per stream type:
const client = new MobulaClient ({
restUrl: 'https://pulse-v2-api.mobula.io' ,
apiKey: 'YOUR_API_KEY' ,
wsUrlMap: {
'pulse-v2' : 'wss://pulse-v2-api.mobula.io' ,
'pulse-v2-pause' : 'wss://pulse-v2-api.mobula.io' ,
},
});
MTT uses this to allow users to configure custom endpoints from the UI (stored in localStorage).
Unsubscribing and Cleanup
Always clean up subscriptions when your component unmounts or when you no longer need the data:
// Store the subscription ID
const subscriptionId = client . streams . subscribe ( 'pulse-v2' , payload , callback );
// Later — unsubscribe cleanly
await client . streams . unsubscribe ( 'pulse-v2' , subscriptionId );
// Or close the entire WebSocket connection for this stream type
client . streams . close ( 'pulse-v2' );
In React, handle this in a cleanup function:
useEffect (() => {
const id = client . streams . subscribe ( 'pulse-v2' , payload , handleMessage );
return () => {
client . streams . unsubscribe ( 'pulse-v2' , id );
};
}, [ payload ]);
Full React Example
Here’s a minimal but complete React hook for Pulse V2, inspired by MTT’s implementation:
import { useEffect , useRef , useState , useMemo , useCallback } from 'react' ;
import { MobulaClient } from '@mobula/sdk' ;
interface PulseToken {
address ?: string ;
chainId ?: string ;
name ?: string ;
symbol ?: string ;
logo ?: string ;
marketCap ?: number ;
holders_count ?: number ;
price_change_24h ?: number ;
created_at ?: string ;
[ key : string ] : unknown ;
}
type ViewName = 'new' | 'bonding' | 'bonded' ;
export function usePulseStream ( apiKey : string , chainIds : string [] = [ 'solana:solana' ]) {
const clientRef = useRef < MobulaClient | null >( null );
const subIdRef = useRef < string | null >( null );
const [ tokens , setTokens ] = useState < Record < ViewName , PulseToken []>>({
new: [], bonding: [], bonded: [],
});
const [ loading , setLoading ] = useState ( true );
const getClient = useCallback (() => {
if ( ! clientRef . current ) {
clientRef . current = new MobulaClient ({
restUrl: 'https://pulse-v2-api.mobula.io' ,
apiKey ,
});
}
return clientRef . current ;
}, [ apiKey ]);
const payload = useMemo (() => ({
assetMode: true ,
compressed: false ,
views: [
{ name: 'new' , model: 'new' as const , chainId: chainIds , limit: 50 },
{ name: 'bonding' , model: 'bonding' as const , chainId: chainIds , limit: 50 },
{ name: 'bonded' , model: 'bonded' as const , chainId: chainIds , limit: 50 },
],
}), [ chainIds ]);
useEffect (() => {
const client = getClient ();
// 1. REST for fast initial load
client . fetchPulseV2 ( payload ). then (( data ) => {
setTokens ({
new: Array . isArray ( data . new ?. data ) ? data . new . data : [],
bonding: Array . isArray ( data . bonding ?. data ) ? data . bonding . data : [],
bonded: Array . isArray ( data . bonded ?. data ) ? data . bonded . data : [],
});
setLoading ( false );
});
// 2. WebSocket for live updates
subIdRef . current = client . streams . subscribe ( 'pulse-v2' , payload , ( msg : any ) => {
if ( msg . type === 'init' ) {
setTokens ({
new: Array . isArray ( msg . payload . new ?. data ) ? msg . payload . new . data : [],
bonding: Array . isArray ( msg . payload . bonding ?. data ) ? msg . payload . bonding . data : [],
bonded: Array . isArray ( msg . payload . bonded ?. data ) ? msg . payload . bonded . data : [],
});
setLoading ( false );
}
if ( msg . type === 'update-token' || msg . type === 'new-token' ) {
const { viewName , token } = msg . payload ;
setTokens (( prev ) => {
const view = prev [ viewName as ViewName ] || [];
const idx = view . findIndex (( t ) => t . address === token . address );
const updated = idx >= 0
? view . map (( t , i ) => ( i === idx ? { ... t , ... token } : t ))
: [ token , ... view ]. slice ( 0 , 50 );
return { ... prev , [viewName]: updated };
});
}
});
return () => {
if ( subIdRef . current ) {
client . streams . unsubscribe ( 'pulse-v2' , subIdRef . current );
subIdRef . current = null ;
}
};
}, [ getClient , payload ]);
return { tokens , loading };
}
Next Steps
Pulse V2 API Reference Full API reference with all parameters and response schemas
Filter Details Complete filter reference with operators and examples
Build Full Pulse UI Step-by-step guide to building a complete Pulse UI with Next.js
MTT Source Code Explore the full open-source trading terminal