Skip to content

WebSocket API

mackinac exposes a single WebSocket endpoint that serves all real-time market data — the same feeds that power the browser interface. Professional subscribers access feeds through their authenticated browser session. API-tier subscribers can additionally authenticate with an API key for headless, programmatic access.

Architecture

┌──────────────────────────────────────────────────────────────┐
│ mackinac data platform │
│ │
│ Hyperliquid L2 ──► HL feed processor ──►┐ │
│ Arbitrum One ──► AMM feed processor ──►┤ Fanout │
│ (on-chain swaps, (Uniswap V3/V4, │ server ────┼──► wss://app.mackinac.io/ws
│ LP events, SushiSwap, │ │
│ pool state) PancakeSwap) ──►┘ │
└──────────────────────────────────────────────────────────────┘
Client authentication
─────────────────────
Browser session ──► JWT (automatic when logged in to the app)
API key ──► { "action": "auth", "key": "mk_live_..." } (API tier only)
Subscription model
──────────────────
Client sends: { "action": "subscribe", "exchange": "hl", "symbol": "BTC" }
Server sends: subscribed → snapshot → quote / print / depth / spread / ... (streaming)

Connecting

Endpoint: wss://app.mackinac.io/ws

Connections are accepted anonymously. Anonymous and browser-session connections receive data gated to the subscriber’s tier. API-tier users must send an auth action immediately after connecting to identify themselves before subscribing.

Authentication (API Tier)

Send immediately after the connection opens:

{ "action": "auth", "key": "mk_live_..." }

Server response on success:

{ "type": "authed", "tier": "api", "symbols": 100 }

Server response on failure:

{ "type": "error", "code": "auth_failed", "message": "Invalid or revoked key" }

Generate and revoke API keys from the Developer section of Account settings in the app. Up to 3 active keys per account. See Subscription Tiers for API tier details.

Subscribing to Symbols

{ "action": "subscribe", "exchange": "hl", "symbol": "BTC" }
{ "action": "subscribe", "exchange": "univ3", "symbol": "WETH/USDC" }
{ "action": "subscribe", "exchange": "univ4", "symbol": "WETH/USDC" }
{ "action": "subscribe", "exchange": "sushiswap", "symbol": "WETH/USDC" }
{ "action": "subscribe", "exchange": "pancake", "symbol": "WETH/USDC" }

Virtual / aggregated streams — require the underlying product to be subscribed first:

{ "action": "subscribe", "exchange": "ammbook", "symbol": "WETH/USDC" }

ammbook aggregates the best bid and offer across all four AMM venues for the pair.

Unsubscribing:

{ "action": "unsubscribe", "exchange": "hl", "symbol": "BTC" }

Symbol Formats

Exchange valueFormatExample
hlStandard perpBTC, ETH
hlMicro contractkPEPE, kSHIB
hlBuilder DEX perpxyz:CL, xyz:NG
hlSpot pairPURR/USDC
univ3 / univ4 / sushiswap / pancakePool pairWETH/USDC, WBTC/USDC

Tier Limits

FreeProfessionalAPI
Max simultaneous symbols250100
quote / print / snapshot
depth
spread
ammbook
liquidity
API key authentication

Data Streams

subscribed

Sent when the server accepts a subscription request.

{ "type": "subscribed", "exchange": "hl", "symbol": "BTC" }

snapshot

Sent immediately after subscribed. Contains a replay of recent prints (up to 1,000) so charts and metrics are populated immediately rather than starting empty. Each item has the same schema as a print message.

{
"type": "snapshot",
"exchange": "hl",
"symbol": "BTC",
"prints": [ { ...print }, { ...print }, ... ]
}

quote

L2 book update. Sent whenever the order book changes.

{
"type": "quote",
"exchange": "hl",
"symbol": "BTC",
"bids": [
{ "price": "97500.0", "size": "2.4" },
{ "price": "97498.0", "size": "5.1" }
],
"asks": [
{ "price": "97501.0", "size": "1.8" },
{ "price": "97503.0", "size": "3.2" }
],
"ts": 1710000000000
}

For AMM products, each level corresponds to one fee-tier pool. Two additional fields are included per level:

FieldDescription
feeTierPool fee rate as a decimal string, e.g. "0.0005" for 0.05%
lastSwapMsMilliseconds since the last swap was observed in this pool

print

A single trade or swap event, annotated with live microstructure metrics computed server-side.

{
"type": "print",
"exchange": "hl",
"symbol": "BTC",
"price": "97500.5",
"size": "0.42",
"side": "A",
"ts": 1710000000123,
"d_tickrate": 0.84,
"d_volumerate": 41200,
"d_lobimb": 0.12,
"d_bidqty": 18.3,
"d_askqty": 15.1,
"d_hawkes": 1.43,
"d_hawkes_bid": 0.61,
"d_hawkes_ask": 0.82,
"d_volumeimb": 0.09,
"d_quoterate": 2.1
}
FieldTypeDescription
sidestring"B" buyer-initiated · "A" seller-initiated
d_tickratenumberTrade arrivals per second at time of print
d_volumeratenumberNotional volume per second at time of print
d_lobimbnumberLOB imbalance (bid_qty − ask_qty) / total — range [−1, +1]
d_bidqtynumberTotal bid-side quantity in the book at time of print
d_askqtynumberTotal ask-side quantity in the book at time of print
d_hawkesnumberHawkes process arrival intensity — all sides
d_hawkes_bidnumberHawkes intensity — bid-hit (seller-initiated) side
d_hawkes_asknumberHawkes intensity — ask-lift (buyer-initiated) side
d_volumeimbnumberSigned volume imbalance (buy − sell volume) in the current window
d_quoteratenumberQuote update rate per second

depth

Uniswap liquidity histogram and market-impact estimates. One message per fee-tier pool. Sent when tick liquidity changes (mint/burn events). Professional+ only.

{
"type": "depth",
"exchange": "univ3",
"symbol": "WETH/USDC",
"feeTier": "0.0005",
"ticks": [
{ "tick": -200760, "liquidity": "18500000000" },
{ "tick": -200820, "liquidity": "12100000000" }
],
"impact": {
"buy": { "1k": 0.04, "10k": 0.18, "100k": 1.42, "1m": 9.3 },
"sell": { "1k": 0.03, "10k": 0.17, "100k": 1.38, "1m": 8.9 }
},
"currentTick": -200761,
"ts": 1710000000000
}
FieldDescription
ticksArray of { tick, liquidity } — active tick boundaries with their liquidity values
currentTickThe pool’s current active tick (where swaps are executing)
impact.buy / impact.sellPrice-impact estimates in basis points for notional sizes of $1k, $10k, $100k, $1M

spread

Cross-pool spread within a single AMM venue — the best bid and best ask available across all fee-tier pools for a pair on that venue. Professional+ only.

{
"type": "spread",
"exchange": "univ3",
"symbol": "WETH/USDC",
"bestBid": { "price": "3499.91", "feeTier": "0.0005" },
"bestAsk": { "price": "3500.09", "feeTier": "0.0001" },
"spreadBps": 5.2,
"ts": 1710000000000
}

ammbook

Consolidated best bid/offer aggregated across all four AMM venues simultaneously. Sent whenever the cross-venue book changes. Professional+ only.

Subscribe with exchange: "ammbook". Requires at least one AMM product for the same pair to already be subscribed.

{
"type": "ammbook",
"symbol": "WETH/USDC",
"bestBid": { "price": "3499.91", "exchange": "univ3", "feeTier": "0.0005" },
"bestAsk": { "price": "3500.07", "exchange": "univ4", "feeTier": "0.0001" },
"spreadBps": 4.6,
"gapGrossBps": 0.0,
"gapNetBps": -5.2,
"venues": [
{ "exchange": "univ3", "bid": "3499.91", "ask": "3500.09", "ageSec": 1 },
{ "exchange": "univ4", "bid": "3499.95", "ask": "3500.07", "ageSec": 0 },
{ "exchange": "sushiswap", "bid": "3499.88", "ask": "3500.12", "ageSec": 4 },
{ "exchange": "pancake", "bid": "3499.82", "ask": "3500.18", "ageSec": 9 }
],
"ts": 1710000000000
}
FieldDescription
bestBid / bestAskThe single best price available across all venues, with source exchange and fee tier
spreadBpsSpread between bestBid and bestAsk in basis points
gapGrossBpsCross-venue spread gap (best bid − best ask across venues) before fees; positive = gap exists
gapNetBpsSame gap after taker fees on both legs; negative means fees exceed the gross gap
venuesPer-venue best bid/ask and seconds since the last swap on that venue

liquidity

LP mint or burn event. Sent when a liquidity provider adds or removes a position. Professional+ only.

{
"type": "liquidity",
"exchange": "univ3",
"symbol": "WETH/USDC",
"event": "mint",
"feeTier": "0.0005",
"amount0": "4.21",
"amount1": "14750.00",
"tickLower": -201000,
"tickUpper": -200400,
"ts": 1710000000000
}
FieldDescription
event"mint" (LP position added) · "burn" (LP position removed)
amount0 / amount1Token quantities added or removed
tickLower / tickUpperThe price range of the position (in Uniswap tick units)

Connection Management Messages

These messages are sent by the server without a client request.

feed_stale

The upstream data feed for a symbol has gone stale. Subsequent quote and print messages for this symbol may be delayed or absent until the feed recovers.

{ "type": "feed_stale", "exchange": "hl", "symbol": "BTC" }

feed_live

The upstream feed has recovered. Normal message flow will resume.

{ "type": "feed_live", "exchange": "hl", "symbol": "BTC" }

server_closing

The server is shutting down gracefully. Reconnect after a short delay.

{ "type": "server_closing", "message": "Planned restart in 10s" }

error

An action was rejected. Sent in response to a specific client action.

{
"type": "error",
"code": "symbol_limit",
"message": "Subscription limit reached for your tier"
}
CodeCause
auth_failedAPI key is invalid, revoked, or expired
symbol_limitActive subscriptions are at the tier maximum
unknown_symbolThe requested symbol was not found
invalid_actionUnrecognized action type or malformed message

Example Session (API Tier)

const ws = new WebSocket('wss://app.mackinac.io/ws');
ws.onopen = () => {
// Step 1 — authenticate with API key
ws.send(JSON.stringify({ action: 'auth', key: 'mk_live_...' }));
};
ws.onmessage = ({ data }) => {
const msg = JSON.parse(data);
if (msg.type === 'authed') {
// Step 2 — subscribe to HL perp and AMM spot
ws.send(JSON.stringify({ action: 'subscribe', exchange: 'hl', symbol: 'ETH' }));
ws.send(JSON.stringify({ action: 'subscribe', exchange: 'univ4', symbol: 'WETH/USDC' }));
// Step 3 — subscribe to aggregated streams
ws.send(JSON.stringify({ action: 'subscribe', exchange: 'ammbook', symbol: 'WETH/USDC' }));
}
if (msg.type === 'print' && msg.exchange === 'hl') {
console.log(
`HL ETH price=${msg.price} hawkes=${msg.d_hawkes.toFixed(3)}` +
` lobImb=${msg.d_lobimb.toFixed(3)} volImb=${msg.d_volumeimb.toFixed(3)}`
);
}
if (msg.type === 'ammbook') {
console.log(
`WETH/USDC best bid=${msg.bestBid.price} (${msg.bestBid.exchange})` +
` best ask=${msg.bestAsk.price} (${msg.bestAsk.exchange})`
);
}
};
ws.onerror = (err) => console.error('WebSocket error', err);
ws.onclose = () => console.log('Disconnected — reconnecting...');