Fetch Market Data

Market data lives under client.api.markets and needs no credentials. Markets are addressed by an integer market_id; perps and spot share the id space, so read market_type rather than guessing from the id.

List Markets

order_books returns every market's static metadata: symbol, type, fees, minimum sizes, and the decimals that prices and sizes are scaled by when you place an order.

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  books = await client.api.markets.order_books()
  for book in books['order_books']:
    print(book['market_id'], book['symbol'], book['market_type'], book['status'])

  perps = await client.api.markets.order_books(filter='perp')
  eth = next(b for b in perps['order_books'] if b['symbol'] == 'ETH')

Market Details And Decimals

order_book_details adds live figures (last price, open interest, 24h volume, margin fractions). Perps and spot come back in separate lists; with a filter, the list of the other market type can be null:

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  details = await client.api.markets.order_book_details(0)
  for perp in details['order_book_details'] or []:
    print(perp['symbol'], perp['last_trade_price'], perp['open_interest'])
    print(perp['supported_price_decimals'], perp['supported_size_decimals'])
  for spot in details['spot_order_book_details'] or []:
    print(spot['symbol'], spot['last_trade_price'])

supported_price_decimals and supported_size_decimals are what Place & Manage Orders scales by. The price_decimals and size_decimals fields are display precision only.

Order Book

A snapshot of the resting orders at the top of the book, best price first on each side:

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  book = await client.api.markets.order_book_orders(market_id=0, limit=20)
  best_bid, best_ask = book['bids'][0], book['asks'][0]
  print(best_bid['price'], best_ask['price'])

For a live book, subscribe to client.streams.order_book instead (Listen To Streams).

Recent Trades

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  trades = await client.api.markets.recent_trades(market_id=0, limit=50)
  for trade in trades['trades'] or []:  # `null` when the market has no trades
    print(trade['timestamp'], trade['price'], trade['size'], trade['type'])

Candles

Candles take a [start, end) range and return at most 500, the newest ones when the range holds more. count_back is a minimum, not a limit: when the range holds fewer candles, Lighter extends it backwards from end, so candles from before start come back too. Times are datetimes both ways:

from datetime import datetime, timedelta, timezone

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  end = datetime.now(timezone.utc)
  candles = await client.api.markets.candles(
    market_id=0,
    resolution='1h',
    start_timestamp=end - timedelta(days=1),
    end_timestamp=end,
    count_back=24,
  )
  for candle in candles['c']:
    print(candle['t'], candle.get('o', 0.0), candle.get('v', 0.0))  # zeros are omitted

  marks = await client.api.markets.mark_price_candles(
    market_id=0, resolution='1h', start_timestamp=end - timedelta(days=1), end_timestamp=end, count_back=24
  )

Lighter leaves zero values out of a candle, so every field but t is optional: read them with .get(..., 0.0). Candle prices and volumes are JSON numbers and come back as float (Numbers).

One response holds at most 500 candles, the newest of the range. Lighter caches candle responses by market, resolution, end_timestamp and count_back, but not by start_timestamp: two requests sharing an end and a count_back get the same rows, whatever their start. So always send the number of candles in [start_timestamp, end_timestamp) as count_back, as above. For a wider range, request consecutive windows of at most 500 candles each, with their boundaries on the resolution's grid (a range holding no candle open is rejected with 22400 invalid timestamps):

from datetime import datetime, timedelta, timezone

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  step = timedelta(hours=1)
  start = datetime(2026, 8, 1, tzinfo=timezone.utc)
  end = datetime(2026, 9, 1, tzinfo=timezone.utc)
  rows = []
  window_end = end
  while window_end > start:
    window_start = max(start, window_end - 500 * step)
    page = await client.api.markets.candles(
      market_id=0,
      resolution='1h',
      start_timestamp=window_start,
      end_timestamp=window_end,
      count_back=(window_end - window_start) // step,
    )
    rows = page['c'] + rows
    window_end = window_start

Funding

from datetime import datetime, timedelta, timezone

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  rates = await client.api.markets.funding_rates()  # Lighter's and other venues' current rates
  for rate in rates['funding_rates']:
    if rate['exchange'] == 'lighter':
      print(rate['market_id'], rate['symbol'], rate['rate'])

  end = datetime.now(timezone.utc)
  history = await client.api.markets.fundings(
    market_id=0, resolution='1h', start_timestamp=end - timedelta(days=1), end_timestamp=end, count_back=24
  )

Exchange-Wide Stats

from typed_lighter import Lighter

async with Lighter.new(public=True) as client:
  stats = await client.api.markets.exchange_stats()     # 24h volume and trades per market
  assets = await client.api.markets.asset_details()     # asset ids and decimals
  status = await client.api.system.status()             # venue status

Upstream reference: Lighter API docs.