Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mobula.io/llms.txt

Use this file to discover all available pages before exploring further.

Mobula indexes perp markets from Gains Network and Lighter. To enumerate them, use the Pulse v2 POST endpoint with poolTypes filters inside a named view. Full schema on the Pulse POST reference.
Supported pool types:
  • gains-perp — Gains Network (Arbitrum, evm:42161)
  • lighter-perp — Lighter L2 (lighter:301, lighter:304)

Endpoint shape

URLPOST https://api.mobula.io/api/2/pulse
Auth headerAuthorization: <YOUR_API_KEY> (raw key, no Bearer)
Body{ "views": [ { name, poolTypes, sortBy, sortOrder, limit, offset } ] }
Response{ "<viewName>": { "data": [...pools] } } — keyed by the name you pass
Each request returns one page (max 100 rows per view). Pagination is offset-based — increment offset by limit until you hit two consecutive empty pages.

Optional: total count via /pulse/pagination

POST https://api.mobula.io/api/2/pulse/pagination accepts the same body and returns { "<viewName>": { "pagination": <number> } }.
For lighter-perp the pagination total matches reality. For gains-perp it can under-report (Gains exposes one base market across many pool variants — Mobula returns the variants too). Treat pagination as a hint; rely on the empty-page break in your loop.

Quick start — curl

curl -X POST "https://api.mobula.io/api/2/pulse" \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "views": [{
      "name": "view",
      "poolTypes": ["gains-perp"],
      "sortBy": "created_at",
      "sortOrder": "desc",
      "limit": 100,
      "offset": 0
    }]
  }'
Lighter only — swap gains-perplighter-perp. Both pool types in one call — pass ["gains-perp", "lighter-perp"] (the response is still keyed by your view name; rows from both types are mixed in data).

TypeScript — paginate the full catalog

import axios from 'axios';

const PULSE_URL = 'https://api.mobula.io/api/2/pulse';
const API_KEY = process.env.MOBULA_API_KEY!;
const PAGE_SIZE = 100;
const HARD_CAP = 5000; // safety stop

type PulsePool = {
  address?: string;
  blockchain?: string;
  tokenSymbol?: string | null;
  tokenName?: string | null;
  latest_price?: number | null;
  liquidity?: number | null;
  volume_24h?: number | null;
};

function buildView(poolType: string, offset: number) {
  return {
    name: 'view',
    poolTypes: [poolType],
    sortBy: 'created_at',
    sortOrder: 'desc',
    limit: PAGE_SIZE,
    offset,
  };
}

async function fetchPage(poolType: string, offset: number): Promise<PulsePool[]> {
  const res = await axios.post<Record<string, { data: PulsePool[] }>>(
    PULSE_URL,
    { views: [buildView(poolType, offset)] },
    { headers: { Authorization: API_KEY, 'Content-Type': 'application/json' } },
  );
  return res.data.view?.data ?? [];
}

async function fetchAll(poolType: string): Promise<PulsePool[]> {
  const all: PulsePool[] = [];
  let offset = 0;
  let emptyStreak = 0;

  while (offset < HARD_CAP) {
    const page = await fetchPage(poolType, offset);
    if (page.length === 0) {
      if (++emptyStreak >= 2) break;
    } else {
      emptyStreak = 0;
      all.push(...page);
    }
    offset += PAGE_SIZE;
  }
  return all;
}

const [gainsRaw, lighter] = await Promise.all([
  fetchAll('gains-perp'),
  fetchAll('lighter-perp'),
]);

console.log(`Gains raw pools: ${gainsRaw.length}`);
console.log(`Lighter rows:    ${lighter.length}`);

DEX-specific normalization

The shape of address and metadata differs by DEX. Two utilities cover the common cases.

Gains — dedupe pool variants into markets

Gains exposes the same trading pair across multiple pool variants (e.g., gains-BTC-USD-0, gains-BTC-USD-1, …). To get a clean per-market list, strip the trailing variant suffix and keep one row per market.
function gainsMarketKey(address?: string): string | null {
  if (!address) return null;
  const parts = address.split('-');
  if (parts.length < 4) return address;
  return parts.slice(0, -1).join('-');     // drop the variant suffix
}

function dedupeGainsByMarket(pools: PulsePool[]): PulsePool[] {
  const seen = new Set<string>();
  const out: PulsePool[] = [];
  for (const p of pools) {
    const key = gainsMarketKey(p.address) ?? p.tokenSymbol ?? '';
    if (!key || seen.has(key)) continue;
    seen.add(key);
    out.push(p);
  }
  return out;
}

const gains = dedupeGainsByMarket(gainsRaw);

Lighter — derive symbol from address when tokenSymbol is null

Some Lighter rows are returned without tokenSymbol. Fall back to parsing the address (e.g., lighter-BTC-USDBTC/USD).
function lighterSymbol(p: PulsePool): string | null {
  if (p.tokenSymbol) return p.tokenSymbol;
  if (p.address?.startsWith('lighter-')) {
    return p.address.slice('lighter-'.length).replace(/-/g, '/');
  }
  return null;
}

Real-time updates — Pulse v2 stream

For live updates instead of (or alongside) the REST snapshot, subscribe to the Pulse v2 WebSocket with the same poolTypes filter:
import { MobulaClient } from '@mobula/sdk';

const client = new MobulaClient({ apiKey: process.env.MOBULA_API_KEY });

client.streams.subscribe(
  'pulse-v2',
  { poolTypes: ['gains-perp', 'lighter-perp'], compressed: false },
  (event) => {
    if (event.type === 'init') console.log('Initial snapshot:', event.payload);
    if (event.type === 'update-token') console.log('Update:', event.payload.token);
  },
);

Common pitfalls

  • Forgetting name on the view. The response is keyed by the name you provide — if you pass name: 'foo', read res.data.foo.data, not res.data.view.data.
  • Stopping at one page. Pulse caps each view page at 100 rows. Loop with offset += 100 until two consecutive empty pages.
  • Trusting the Gains pagination total. pulse/pagination under-counts for gains-perp because of pool variants. Use the empty-page break as ground truth; dedupe afterwards.
  • Empty tokenSymbol on Lighter rows. Fall back to parsing address (lighter-BTC-USDBTC/USD).
  • Bearer prefix in the Authorization header. Don’t add it. Pulse expects the raw API key.

Pulse POST reference

Full request/response schema for /api/2/pulse.

Perp Quote

Quote a perp position once you’ve picked a market.

Execute Perp Action

Submit signed payloads via /2/perp/execute-v2.

Perps Data Model

Indexed event shapes for perps.

Need help?

Telegram

Fast support

Discord

Community

Slack

Enterprise

Email

Contact us