MM Flow

mmflow API · Cookbook

Cookbook

End-to-end recipes — every snippet is a complete, runnable mini-project. Drop it next to the SDK file, set one env var, and it runs. TypeScript + Python side-by-side.

Whale alert Discord bot

Discord webhook + SSE stream

Post to a Discord channel every time someone takes a $250k+ position on Hyperliquid.

Setup

  1. Create a Discord webhook in your channel settings (Edit Channel → Integrations → Webhooks).
  2. Set DISCORD_WEBHOOK environment variable to the webhook URL.
  3. Run the script. Reconnects automatically on disconnect.
// whale-bot.ts — Bun / Deno / Node 18+
import { MmflowClient } from "./mmflow";

const WEBHOOK = process.env.DISCORD_WEBHOOK!;
const MIN_USD = 250_000;

const mm = new MmflowClient();

async function post(content: string) {
  await fetch(WEBHOOK, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content }),
  });
}

while (true) {
  const { events } = mm.streamWhales({ minUsd: MIN_USD });
  try {
    for await (const ev of events) {
      if (ev.type !== "whale") continue;
      const w = ev.data;
      const arrow = w.side === "buy" ? "📈" : "📉";
      const verb = w.side === "buy" ? "long" : "short";
      const usd = (w.notionalUsd / 1e6).toFixed(2) + "M";
      await post(`${arrow} ${w.coin} ${verb} $${usd} @ $${w.price} — ${w.venue}`);
    }
  } catch (err) {
    console.error("stream ended, reconnecting in 3s", err);
    await new Promise((r) => setTimeout(r, 3000));
  }
}

Expected output

📈 BTC long $1.28M @ $64,320 — hyperliquid
📉 HYPE short $420k @ $28.41 — hyperliquid
📈 SOL long $310k @ $145.20 — hyperliquid

Deploy

PM2 / systemd / Docker — the script is a long-running process. Memory footprint stays under 50 MB.

Daily CSV snapshot

Read-only one-shot · cron-friendly

Once a day, dump the current OI + funding + 24h liquidations table to a CSV file. Drop into cron.

Setup

  1. Save as snapshot.{ts,py} next to mmflow.{ts,py}.
  2. Run by hand once to verify output, then add to cron (e.g. 0 0 * * * /path/to/snapshot.py).
  3. Files land at ./snapshots/YYYY-MM-DD.csv.
// snapshot.ts
import { writeFile, mkdir } from "node:fs/promises";
import { MmflowClient } from "./mmflow";

const mm = new MmflowClient();
const today = new Date().toISOString().slice(0, 10);
await mkdir("snapshots", { recursive: true });

const [oi, funding, liqs] = await Promise.all([
  mm.perpsOi(),
  mm.perpsFunding(),
  mm.perpsLiquidations(),
]);

const fundBy = new Map(funding.data.map((r) => [r.coin, r.rate]));
const liqBy = new Map(liqs.data.map((r) => [r.coin, r]));

const rows = [
  "coin,oi_usd,funding_8h_bps,liq24h_long_usd,liq24h_short_usd",
  ...oi.data.map((r) => {
    const f = fundBy.get(r.coin) ?? 0;
    const l = liqBy.get(r.coin);
    // Convert 1h rate → 8h bps for human-readable column.
    const fund8hBps = (f * 8 * 1e4).toFixed(2);
    return [
      r.coin,
      r.oiUsd.toFixed(2),
      fund8hBps,
      (l?.long24hUsd ?? 0).toFixed(0),
      (l?.short24hUsd ?? 0).toFixed(0),
    ].join(",");
  }),
].join("\n");

await writeFile(`snapshots/${today}.csv`, rows);
console.log(`wrote snapshots/${today}.csv with ${oi.data.length} rows`);

Expected output

coin,oi_usd,funding_8h_bps,liq24h_long_usd,liq24h_short_usd
BTC,1284390210.50,0.83,18402100,9281400
ETH,418394100.20,1.21,9120400,4502100
SOL,142028104.10,2.04,3812400,1284900
…

Deploy

cron, GitHub Actions schedule, Vercel Cron, Cloudflare Workers Cron — any scheduler with HTTPS egress.

Funding-arb scanner

Cross-venue compare · terminal one-liner

Find the largest HL vs Binance funding spread right now. Print top 5 sorted by annualized arb APR.

Setup

  1. Drop alongside the SDK file.
  2. Run on demand — no scheduler needed.
  3. Calls /perps/funding with venues=hl,binance,bybit so the scanner has real cross-venue rates to compute spreads.
// arb-scan.ts
import { MmflowClient } from "./mmflow";

const mm = new MmflowClient();
const { data } = await mm.perpsFunding({
  venues: ["hl", "binance", "bybit"],
});

// Group rows by coin → venue → annualized APR.
const apr = (rate: number, hours: number) =>
  rate * (24 / hours) * 365 * 100;
const byCoin = new Map<string, Record<string, number>>();
for (const r of data) {
  const cur = byCoin.get(r.coin) ?? {};
  cur[r.venue] = apr(r.rate, r.periodHours);
  byCoin.set(r.coin, cur);
}

// Compute spread = HL minus best CEX leg per coin.
const spreads: Array<{ coin: string; hl: number; cexVenue: string; cex: number; spreadPp: number }> = [];
for (const [coin, venues] of byCoin) {
  const hl = venues["hyperliquid"];
  if (hl == null) continue;
  const cexEntries = (["binance", "bybit"] as const)
    .map((v) => ({ v, apr: venues[v] }))
    .filter((x) => x.apr != null);
  if (cexEntries.length === 0) continue;
  // Pick CEX leg with the OPPOSITE sign to HL — that's the arb.
  const oppSign = cexEntries.find((x) => Math.sign(x.apr) !== Math.sign(hl))
    ?? cexEntries.reduce((a, b) => (Math.abs(a.apr) < Math.abs(b.apr) ? a : b));
  spreads.push({ coin, hl, cexVenue: oppSign.v, cex: oppSign.apr, spreadPp: hl - oppSign.apr });
}
spreads.sort((a, b) => Math.abs(b.spreadPp) - Math.abs(a.spreadPp));

console.log("Top HL-vs-CEX funding spreads (annualized):");
for (const s of spreads.slice(0, 5)) {
  const sign = (n: number) => (n >= 0 ? "+" : "");
  console.log(
    `  ${s.coin.padEnd(6)} HL ${sign(s.hl)}${s.hl.toFixed(1)}% APR  ${s.cexVenue.padEnd(7)} ${sign(s.cex)}${s.cex.toFixed(1)}%  spread ${sign(s.spreadPp)}${s.spreadPp.toFixed(1)}pp`,
  );
}

Expected output

Top HL-vs-CEX funding spreads (annualized):
  ETH    HL +49.3% APR  binance +18.2%  spread +31.1pp
  SOL    HL +42.1% APR  bybit   +12.4%  spread +29.7pp
  ARB    HL +38.9% APR  binance +14.8%  spread +24.1pp
  AVAX   HL +33.1% APR  binance +11.6%  spread +21.5pp
  XRP    HL +28.4% APR  bybit   +8.9%   spread +19.5pp

Deploy

One-shot terminal command — no background process.

Gamma-flip break alert

SSE + REST blend · vol-regime alerts

Watch BTC funding stream + GEX. Alert when BTC spot crosses the gamma flip strike (vol regime change).

Setup

  1. Set TELEGRAM_TOKEN + TELEGRAM_CHAT_ID for notifications, or replace post() with any alerting transport.
  2. Run as a long-lived process. Re-fetches GEX every 5 minutes; reads trades stream continuously.
// gamma-flip.ts
import { MmflowClient } from "./mmflow";

const mm = new MmflowClient();
const TG = process.env.TELEGRAM_TOKEN!;
const CHAT = process.env.TELEGRAM_CHAT_ID!;

async function notify(msg: string) {
  console.log("→ posted:", msg);
  if (!TG) return;
  await fetch(`https://api.telegram.org/bot${TG}/sendMessage`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chat_id: CHAT, text: msg }),
  });
}

let flip: number | null = null;
let lastPrice: number | null = null;

async function refreshGex() {
  const { data } = await mm.optionsGex({ underlying: "BTC" });
  flip = data.gammaFlip;
  console.log(`[gex refreshed] BTC flip = $${flip?.toFixed(0) ?? "n/a"}`);
}
await refreshGex();
setInterval(refreshGex, 300_000);

const { events } = mm.streamTrades({ coin: "BTC" });
for await (const ev of events) {
  if (ev.type !== "trade" || flip == null) continue;
  const p = ev.data.price;
  if (lastPrice != null && lastPrice < flip && p >= flip) {
    await notify(`🟢 BTC crossed UP through gamma flip @ $${p.toFixed(0)} (negative-γ → positive-γ regime)`);
  } else if (lastPrice != null && lastPrice > flip && p <= flip) {
    await notify(`🔴 BTC crossed DOWN through gamma flip @ $${p.toFixed(0)} (positive-γ → negative-γ regime)`);
  }
  lastPrice = p;
}

Expected output

[gex refreshed] BTC flip = $63,800
[trade] BTC buy @ $63,810 → crossing UP through flip!
→ posted: 🟢 BTC crossed UP through gamma flip @ $63,810
            (negative-γ → positive-γ regime; vol suppression ahead)

Deploy

PM2 / systemd / Docker. ~50 MB memory.

Telegram funding-spike alert

Telegram Bot API + funding polling

Fire a Telegram message whenever any tracked HL coin's funding rate crosses ±5bp.

Setup

  1. Create a Telegram bot via @BotFather → get the bot token.
  2. Find your chat ID (or group ID) — easiest via @userinfobot.
  3. Set TG_BOT_TOKEN and TG_CHAT_ID env vars. Optionally pass a COINS list (comma-separated).
// funding-alert.ts — Bun / Deno / Node 18+
import { MmflowClient } from "./mmflow";

const TOKEN = process.env.TG_BOT_TOKEN!;
const CHAT = process.env.TG_CHAT_ID!;
const COINS = (process.env.COINS ?? "BTC,ETH,SOL,HYPE").split(",");
const THRESHOLD_BPS = 5;

const mm = new MmflowClient();
const lastFlag: Record<string, string | null> = {};

async function tg(msg: string) {
  await fetch(`https://api.telegram.org/bot${TOKEN}/sendMessage`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chat_id: CHAT, text: msg }),
  });
}

setInterval(async () => {
  for (const coin of COINS) {
    const f = await mm.perpsFunding({ coin });
    const bps = f.venues.hyperliquid.rate * 10_000;
    const flag = bps > THRESHOLD_BPS ? "long" : bps < -THRESHOLD_BPS ? "short" : null;
    if (flag && flag !== lastFlag[coin]) {
      const verb = flag === "long" ? "long-pay" : "short-pay";
      await tg(`⚡ ${coin} funding spiked: ${verb} ${bps.toFixed(1)}bp/h (HL)`);
    }
    lastFlag[coin] = flag;
  }
}, 60_000);

Expected output

⚡ BTC funding spiked: long-pay 7.2bp/h (HL)
⚡ ETH funding flipped: short-pay -6.1bp/h (HL)

Deploy

Cron-able. The script polls every 60s and only fires on transitions across the ±5bp threshold so you don't get spammed every minute the rate stays elevated.

Jupyter OI-regime classifier

Notebook research — daily run

Pull /perps/oi/history, fit a 180-day rolling z-score, and label the current regime as compressed / extended / stretched.

Setup

  1. Drop the snippet into a fresh Jupyter notebook.
  2. Install numpy and the mmflow SDK (`pip install mmflow-sdk numpy`).
  3. Run the cell. The output is a one-line regime label for each coin in COINS.
// regime.ts — same logic, TS variant
import { MmflowClient } from "./mmflow";

const mm = new MmflowClient();
const coins = ["BTC", "ETH", "SOL", "HYPE"];

for (const coin of coins) {
  const { points } = await mm.perpsOiHistory({ coin, days: 180 });
  const series = points.map((p) => p.oi);
  const mean = series.reduce((s, x) => s + x, 0) / series.length;
  const sd = Math.sqrt(series.reduce((s, x) => s + (x - mean) ** 2, 0) / series.length);
  const z = (series[series.length - 1] - mean) / sd;
  const regime = Math.abs(z) < 0.5 ? "compressed" : z > 1.5 ? "stretched" : "extended";
  console.log(`${coin.padEnd(4)} z=${z.toFixed(2).padStart(6)} → ${regime}`);
}

Expected output

BTC  z= 1.82 → stretched
ETH  z= 0.31 → compressed
SOL  z=-1.15 → extended
HYPE z= 2.41 → stretched

Deploy

Schedule the notebook via Papermill + cron for a daily regime feed. Pipe the output to your Discord / Notion / spreadsheet.

Vol-surface fitter

Quant research — daily cron

Pull /options/skew daily, fit a cubic spline through the delta-bucketed IVs, persist a daily snapshot.

Setup

  1. pip install mmflow-sdk numpy scipy
  2. Set DATA_DIR to the directory where snapshots should land (one CSV per day).
  3. Run via cron at 00:05 UTC for clean day boundaries.
// vol-surface.ts — TS variant (uses simple linear interp; for spline,
// use a numeric library like ml-spline).
import { writeFileSync, mkdirSync } from "fs";
import { MmflowClient } from "./mmflow";

const mm = new MmflowClient();
const dir = process.env.DATA_DIR ?? "data";
mkdirSync(dir, { recursive: true });

for (const ccy of ["BTC", "ETH"]) {
  const skew = await mm.optionsSkew({ ccy });
  const points = skew.points
    .map((p) => `${p.delta},${p.dte},${p.iv.toFixed(4)}`)
    .join("\n");
  const day = new Date().toISOString().slice(0, 10);
  writeFileSync(`${dir}/${day}-${ccy.toLowerCase()}-vol.csv`, `delta,dte,iv\n${points}`);
  console.log(`${ccy} vol surface snapshot saved → ${dir}/${day}-${ccy.toLowerCase()}-vol.csv`);
}

Expected output

BTC vol surface snapshot saved → data/2026-05-18-btc-vol.csv
ETH vol surface snapshot saved → data/2026-05-18-eth-vol.csv

Deploy

Cron daily. The CSV files build a vol-surface time-series you can backtest against — fit per-day, then compare today's surface vs the 30-day median for cheap/rich identification.

Embeddable HL ticker widget

Embed in your community Discord forum or docs site

A self-contained HTML snippet a forum admin can drop into Discord / Discourse / a docs page — shows live HL whale prints.

Setup

  1. Paste the iframe snippet anywhere that supports HTML — Discourse forum post, Notion embed, your own site.
  2. Adjust ?minUsd= for the threshold. The widget auto-reconnects on disconnect.
<!-- Anywhere HTML is allowed: forum post, Notion embed, your site -->
<iframe
  src="https://mmflow.ai/embed/whale-ticker?minUsd=100000"
  width="100%"
  height="48"
  frameborder="0"
  loading="lazy"
  title="HL whale ticker"
></iframe>

<!-- Filter to a specific coin -->
<iframe
  src="https://mmflow.ai/embed/whale-ticker?minUsd=50000&coin=HYPE"
  width="100%"
  height="48"
  frameborder="0"
  loading="lazy"
></iframe>

Expected output

A 48px-tall continuously-scrolling ticker showing every HL print above your threshold.

Deploy

No deployment needed. The widget hits mmflow.ai/embed/whale-ticker directly — we serve the chrome, you embed the iframe.

VS Code status-bar funding extension

Local IDE integration — for traders writing code

Show the current HL funding rate for a watched coin in your editor's status bar — a ~300-line VS Code extension stub.

Setup

  1. Scaffold: `yo code` → New TypeScript Extension.
  2. Drop the snippet into `src/extension.ts` and `package.json` configuration.
  3. F5 to launch in an Extension Development Host.
// src/extension.ts
import * as vscode from "vscode";

const COIN = vscode.workspace.getConfiguration("mmflow").get<string>("coin", "BTC");
let status: vscode.StatusBarItem;

export function activate(ctx: vscode.ExtensionContext) {
  status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
  status.command = "mmflow.openLive";
  status.show();
  ctx.subscriptions.push(status);
  ctx.subscriptions.push(
    vscode.commands.registerCommand("mmflow.openLive", () =>
      vscode.env.openExternal(vscode.Uri.parse("https://mmflow.ai/live"))
    ),
  );

  refresh();
  const interval = setInterval(refresh, 60_000);
  ctx.subscriptions.push({ dispose: () => clearInterval(interval) });
}

async function refresh() {
  try {
    const r = await fetch(`https://mmflow.ai/api/v1/perps/funding?coin=${COIN}`);
    const f = await r.json();
    const bps = f.venues.hyperliquid.rate * 10_000;
    const sign = bps >= 0 ? "+" : "";
    status.text = `${COIN} fnd ${sign}${bps.toFixed(1)}bp`;
    status.color = bps > 5 ? "#34d399" : bps < -5 ? "#ef4444" : undefined;
  } catch {
    status.text = `${COIN} fnd —`;
  }
}

Expected output

Status bar shows: `BTC fnd +4.2bp` — refreshes every 60s, color-codes green for long-pay and pink for short-pay.

Deploy

Package with `vsce package` and publish to the marketplace. Or `code --install-extension <vsix>` for personal use.

MetaMask Snap — gamma-flip wallet alert

On-chain wallet UX

Surface a gamma-flip break notification inside MetaMask. Reads the user's BTC balance and pings when price crosses the dealer gamma flip level.

Setup

  1. Init a Snap project: `yarn create @metamask/snap`.
  2. Replace `src/index.ts` with the snippet below and `snap.manifest.json` permissions.
  3. Test via the MetaMask Flask developer build.
// src/index.ts — MetaMask Snap stub
import type { OnCronjobHandler } from "@metamask/snaps-sdk";

const COIN = "BTC";
let lastPrice: number | null = null;
let lastFlip: number | null = null;

export const onCronjob: OnCronjobHandler = async ({ request }) => {
  if (request.method !== "tick") return;

  // 1. Pull current mid + gamma flip from mmflow.
  const [midRes, gexRes] = await Promise.all([
    fetch(`https://mmflow.ai/api/v1/perps/mids?coin=${COIN}`),
    fetch(`https://mmflow.ai/api/v1/options/gex?ccy=${COIN}`),
  ]);
  const mid = (await midRes.json()).price;
  const gex = await gexRes.json();
  const flip: number | null = gex.gammaFlip ?? null;

  // 2. Detect a crossing vs the previous tick.
  if (lastPrice != null && flip != null) {
    if (lastPrice < flip && mid >= flip) {
      await notify(`BTC crossed UP through gamma flip @ $${mid.toFixed(0)}`);
    } else if (lastPrice > flip && mid <= flip) {
      await notify(`BTC crossed DOWN through gamma flip @ $${mid.toFixed(0)}`);
    }
  }
  lastPrice = mid;
  lastFlip = flip;
};

async function notify(message: string) {
  await snap.request({
    method: "snap_notify",
    params: { type: "native", message },
  });
}

Expected output

MetaMask drops a notification: "BTC crossed UP through gamma flip @ $67,140 — dealer hedging regime change. Open mmflow /live for context."

Deploy

Publish the Snap to npm under the @your-handle/mmflow-snap name. Users add it via the Snaps directory. Anyone holding BTC sees a wallet-level notification on each gamma flip break.

Got a recipe to share?

Open a PR with your recipe and we'll feature it here. Cookbook entries are MIT-licensed and credited to the author. REST reference