Python client for the Mackinac market-data API — live quotes, trades, funding rates, yield rates, and lending markets across 16 DeFi venues including Hyperliquid, dYdX V4, Uniswap V3/V4, Pendle, Spectra, GMX, Vertex, Ostium, Aave V3, Compound V3, Morpho Blue, and more.
pip install mackinac-client
No credentials required for free-tier access (3 concurrent symbols):
from mackinac import Mackinac, QuoteMessage, PrintMessage
with Mackinac() as m:
# What symbols are on HL?
print(m.live_symbols("hl"))
# Stream only quotes — types= filters the feed, no isinstance needed
with m.subscribe("hl:ETH", types=QuoteMessage) as feed:
for quote in feed:
bid = quote.bids[0].price if quote.bids else None
ask = quote.asks[0].price if quote.asks else None
print(f"ETH bid {bid} ask {ask}")
break # just the first one
# Stream only trades
with m.subscribe("hl:ETH", types=PrintMessage) as feed:
for trade in feed:
side = "buy" if trade.side == 1 else "sell"
print(f"ETH {side} {trade.price} × {trade.size}")
breakimport asyncio
from mackinac import AsyncClient, QuoteMessage
async def main():
async with AsyncClient() as client:
print(await client.live_symbols("hl"))
async with client.subscribe("hl:ETH", types=QuoteMessage) as feed:
async for quote in feed:
print(f"ETH bid {quote.bids[0].price} ask {quote.asks[0].price}")
break
asyncio.run(main())Both Mackinac (sync) and AsyncClient (async) share the same constructors:
m = Mackinac.from_api_key("mk_live_...") # sync
client = AsyncClient.from_api_key("mk_live_...") # asyncAPI keys are long-lived and authenticate WebSocket connections only.
Obtain them from the dashboard (requires tier api).
m = Mackinac.from_jwt("eyJhbGciOi...")
client = AsyncClient.from_jwt("eyJhbGciOi...")JWTs expire after 7 days. Refresh via client.refresh_token() / await client.refresh_token().
pip install 'mackinac-client[wallet]'m = Mackinac.from_wallet(private_key="0xdeadbeef...", address="0xYourWalletAddress")
client = await AsyncClient.from_wallet(
private_key="0xdeadbeef...",
address="0xYourWalletAddress",
)async with client.subscribe("hl:ETH", "uni:WETH/USDC", "rates:all") as feed:
async for msg in feed:
match msg.type:
case "quote":
print(msg.exchange, msg.symbol, msg.bids[0].price)
case "print":
print(msg.price, msg.size, msg.side) # side: 0=sell 1=buy
case "rate_market":
print(msg.symbol, f"{msg.impliedApy * 100:.2f}% APY")
case "funding":
print(msg.symbol, f"{msg.ratePct:.4f}% ann.")All messages are typed Pydantic v2 models. Use isinstance() or match
on msg.type to branch per message type.
| Venue | Key format | Example |
|---|---|---|
| Hyperliquid perp | hl:<SYMBOL> |
hl:ETH |
| dYdX V4 perp | dydx:<BASE>-USD |
dydx:ETH-USD |
| Uniswap V3 (Arbitrum) | uni:<BASE>/<QUOTE> |
uni:WETH/USDC |
| Uniswap V4 (Arbitrum) | univ4:<BASE>/<QUOTE> |
univ4:WETH/USDC |
| SushiSwap V3 | sushi:<BASE>/<QUOTE> |
sushi:WETH/USDC |
| PancakeSwap V3 | pancake:<BASE>/<QUOTE> |
pancake:WBTC/WETH |
| GMX perp | gmx:<SYMBOL> |
gmx:BTC |
| Vertex perp | vertex:<SYMBOL> |
vertex:SOL |
| Ostium (RWA) | ostium:<BASE>/<QUOTE> |
ostium:XAU/USD |
| Aave V3 lending | aave:<ASSET> or aave:all |
aave:USDC |
| Compound V3 lending | compound:<ASSET> or compound:all |
compound:USDC |
| Morpho Blue lending | morpho:<ASSET> or morpho:all |
morpho:USDC |
| Lending actions (all venues) | lending:actions |
lending:actions |
| Pendle/Spectra (all) | rates:all |
— |
| Pendle/Spectra (trades) | rates:swaps |
— |
| Specific yield market | rates:<symbol-or-address> |
rates:PT-weETH-25JUN2026 |
| AMM consolidated NBBO | ammbook:<BASE>/<QUOTE> |
ammbook:WETH/USDC (professional+) |
Use mackinac.symbols helpers to construct keys safely:
from mackinac import symbols
symbols.amm_pair("WETH", "USDC") # "WETH/USDC"
symbols.ostium_pair("XAU") # "XAU/USD"
symbols.pendle_address("0xC62D...") # "0xc62d..." (lowercased)
symbols.rates_all() # "rates:all"await feed.unsubscribe("hl:ETH")The library reconnects automatically on drops (1s → 2s → 4s → … → 30s cap)
and re-subscribes all active symbols. The async for loop continues
seamlessly — no user-side retry logic needed.
If you connect with the websockets library directly (rather than via AsyncClient),
set max_size to at least 10 MB. The initial trade snapshot sent on subscribe can
exceed the library's 1 MB default:
async with websockets.connect(url, max_size=10 * 1024 * 1024) as ws:
...AsyncClient already sets this internally.
# Which exchanges are live?
await client.markets() # dict[exchange, MarketStatus]
# All subscribable symbols on one venue
await client.live_symbols("hl") # list[str]
await client.live_symbols("pendle") # list[str]
# All venues at once
await client.live_symbols() # dict[exchange, list[str]]
# Symbols with persisted history
await client.historical_symbols("hl") # list[str]
# Which venues quote ETH?
await client.instrument("ETH") # InstrumentVenuesHistory iterators paginate automatically — no cursor handling needed:
# Trades
async for trade in client.history_trades("hl", "ETH",
start="2026-04-01",
end="2026-04-08"):
print(trade.time, trade.price, trade.size, trade.side)
# 5-second quote snapshots
async for snap in client.history_quotes("uni", "WETH/USDC",
start="2026-04-01"):
print(snap.time, snap.bids[0].price if snap.bids else None)
# Funding rates
async for fr in client.history_funding("hl", "ETH"):
print(fr.ratePct, fr.intervalHrs)
# Yield-market snapshots
async for rate in client.history_rates("0xc62d...", start="2026-03-01"):
print(rate.impliedApy, rate.tvl)All methods accept start/end as ISO 8601 strings or epoch milliseconds.
Default range: last 24 hours. Maximum lookback: 1 day (free tier) or 90 days (professional/api).
await client.me() # JWT claims
await client.subscription_status() # current tier + expiry (re-reads on-chain)
await client.refresh_token() # new 7-day JWT with updated tier claims| Type | Description | Venues |
|---|---|---|
QuoteMessage |
Top-of-book snapshot | All |
PrintMessage |
Trade execution | CLOB, AMM, Oracle*, Yield |
FundingMessage |
Perpetual funding rate | HL, dYdX, GMX, Vertex, Ostium |
DepthMessage |
Concentrated-liquidity tick snapshot + market impact | AMM |
LiquidityMessage |
LP Mint/Burn event | AMM |
RateMarketMessage |
Yield-market or lending-market snapshot | Pendle, Spectra, Aave, Compound, Morpho |
RateDepthMessage |
Depth-at-size APY quote | Pendle only |
LendingActionMessage |
On-chain lending event (Supply/Borrow/Liquidate/...) | Aave, Compound, Morpho |
RateModelParamsMessage |
IRM curve parameters | Aave, Compound, Morpho |
AmmBookMessage |
Consolidated AMM NBBO (professional+) | Virtual |
AmmLiquiditySnapshotMessage |
LP event backfill on subscribe (professional+) | Virtual |
ArbFlagMessage |
Cross-venue arb gap signal (super_admin) | Virtual |
SpreadMessage |
Same-venue cross-tier spread (professional+) | Uni, Univ4 |
FeedStaleMessage |
Feed health: stale | Broadcast |
FeedLiveMessage |
Feed health: recovered | Broadcast |
ServerClosingMessage |
Graceful shutdown notice | Broadcast |
ErrorMessage |
Error frame | Server response |
*GMX and Vertex do not emit PrintMessage (oracle settle on net positions).
time— always epoch milliseconds (int)side—0= aggressive sell (bid hit),1= aggressive buy (ask lift),2= unknownratePct(FundingMessage) — annualized percentage, e.g.10.95= 10.95%/yrimpliedApy(RateMarketMessage) — decimal fraction, e.g.0.058= 5.8%ptPrice— Pendle: USD price; Spectra: fraction of IBTtvl— Pendle: USD; Spectra v1: underlying-asset units (no USD oracle)amount0/amount1/amount(LiquidityMessage) — decimal strings in raw ERC-20 units
| Tier | WS symbol cap | ammbook / spread |
Historical data |
|---|---|---|---|
| Free (anonymous) | 3 | — | 1 day |
| Professional | 50 | ✓ | 90 days |
| API | 100 | ✓ | 90 days |
from mackinac.exceptions import (
AuthError, # 401 / WS auth_error
TierError, # 403 / WS subscription_required
RateLimitError, # 429 / WS rate_limited — has .retry_after
SymbolLimitError,
InvalidSymbolError,
ServerError, # 5xx / WS internal_error
)
try:
async with client.subscribe("ammbook:WETH/USDC") as feed:
...
except TierError:
print("Upgrade to professional tier for ammbook access")- REST 4xx → typed exception immediately (no retry)
- REST 5xx → retried 3× with exponential backoff, then
ServerError - WS
auth_error/subscription_required→ raised, reconnect stops - Other WS errors → delivered as
ErrorMessageto the iterator
pip install 'mackinac-client[wallet]' # EIP-191 wallet sign-in
pip install 'mackinac-client[tables]' # pandas, for amm_fee_table + rate_table examples
pip install 'mackinac-client[dev]' # testing + model codegenExamples with both API flavours use a _sync / _async filename suffix.
Older single-flavour examples keep their original names.
| File | What it shows |
|---|---|
examples/subscribe_quotes.py |
HL ETH live top-of-book |
examples/historical_trades.py |
7-day Ostium XAU/USD → CSV |
examples/yield_rates.py |
Pendle + Spectra live APY table |
examples/reconnect_pattern.py |
Iterator survives forced disconnect |
examples/multi_venue_basis.py |
Cross-venue ETH basis across CLOB, AMM, and oracle venues |
examples/amm_fee_table.py |
AMM depth → pandas fee-tier impact table |
examples/rate_table.py |
rate_market stream → pandas DataFrame |
examples/funding_carry.py |
CME vs HL vs Ostium carry cost comparison |
examples/lending_rates_sync.py |
Aave + Compound + Morpho supply/borrow APY table (sync Mackinac) |
examples/lending_rates_async.py |
Same lending-rate table using AsyncClient |
examples/liquidation_watch_sync.py |
Live liquidation feed across all lending venues (sync) |
examples/liquidation_watch_async.py |
Same liquidation feed using AsyncClient |
mackinac.__version__ follows the library release.
mackinac.__protocol_version__ matches the API schema version; a major bump
means a breaking wire-format change.
MIT