Skip to main content

Market Data Streaming

Subscribe to real-time market data updates including order book depth, instrument states, and trade statistics using Python and gRPC.
No Participant ID RequiredThis streaming endpoint only requires Auth0 JWT authentication with read:marketdata scope. You do not need to provide the x-participant-id header or complete KYC onboarding to access market data streams.

Service Definition

Service: polymarket.v1.MarketDataSubscriptionAPI RPC: CreateMarketDataSubscription Type: Server-side streaming
service MarketDataSubscriptionAPI {
    rpc CreateMarketDataSubscription(CreateMarketDataSubscriptionRequest)
        returns (stream CreateMarketDataSubscriptionResponse);
}

Request Parameters

CreateMarketDataSubscriptionRequest

FieldTypeRequiredDescription
symbolslist[str]NoList of symbols to subscribe to. Empty list subscribes to all instruments.
unaggregatedboolNoIf True, receive raw order book. If False (default), receive aggregated book by price level.
depthintNoNumber of price levels to include in order book. Default: 10
snapshot_onlyboolNoIf True, receive only initial snapshot then close stream. If False (default), receive continuous updates.

Example Request

from polymarket.v1 import marketdatasubscription_pb2

# Subscribe to specific symbols
request = marketdatasubscription_pb2.CreateMarketDataSubscriptionRequest(
    symbols=["tec-nfl-sbw-2026-02-08-kc", "tec-nfl-sbw-2026-02-08-phi"],
    unaggregated=False,
    depth=10,
    snapshot_only=False
)

# Subscribe to all symbols
request = marketdatasubscription_pb2.CreateMarketDataSubscriptionRequest(
    symbols=[],  # Empty = all symbols
    depth=20
)

Response Messages

The stream returns CreateMarketDataSubscriptionResponse messages with two possible event types:

1. Heartbeat Messages

Keep-alive messages to confirm connection is active.
if response.HasField('heartbeat'):
    print(f"[{datetime.now().strftime('%H:%M:%S')}] Heartbeat received")
If you stop receiving heartbeats, the connection may be stale. Consider reconnecting.

2. Market Data Updates

Real-time market data changes.
if response.HasField('update'):
    update = response.update
    print(f"Symbol: {update.symbol}")
    print(f"State: {update.state}")
    print(f"Bids: {len(update.bids)}")
    print(f"Offers: {len(update.offers)}")

Update Message Structure

Fields

FieldTypeDescription
symbolstrInstrument symbol (e.g., “tec-nfl-sbw-2026-02-08-kc”)
bidslist[BookEntry]Bid side of order book (buy orders)
offerslist[BookEntry]Offer/ask side of order book (sell orders)
stateInstrumentStateCurrent trading state of instrument
statsInstrumentStatsMarket statistics (optional)
transact_timeTimestampServer timestamp of update
book_hiddenboolIf True, order book is hidden

BookEntry Structure

Each price level in the order book contains:
FieldTypeDescription
pxint64Price as integer (divide by price_scale)
qtyint64Aggregate quantity at this price level
Price Representation: All prices are int64 values. Divide by the instrument’s price_scale to get the decimal value.price_scale varies by instrument. Query instrument metadata to get the correct value.
px = bid.px / price_scale # Convert from price representation
print(f"${px:.4f}")

InstrumentStats Structure

Market statistics include:
FieldTypeDescription
last_trade_pxint64Last trade price (÷ price_scale)
open_pxint64Opening price (÷ price_scale)
high_pxint64High price of session (÷ price_scale)
low_pxint64Low price of session (÷ price_scale)
close_pxint64Closing price (÷ price_scale)
shares_tradedint64Total volume traded
open_interestint64Current open interest
notional_tradedint64Total notional value traded
Stats fields use protobuf oneof, so they may not always be present. Always check with HasField() before accessing.
if update.HasField('stats') and update.stats.HasField('last_trade_px'):
    last_px = update.stats.last_trade_px / price_scale

Instrument States

StateValueDescription
INSTRUMENT_STATE_CLOSED0Market closed, no trading allowed
INSTRUMENT_STATE_OPEN1Active trading, continuous matching
INSTRUMENT_STATE_PREOPEN2Orders accepted but no matching (opening auction)
INSTRUMENT_STATE_SUSPENDED3Trading suspended, cancel-only
INSTRUMENT_STATE_EXPIRED4Instrument expired, no new orders
INSTRUMENT_STATE_TERMINATED5Instrument terminated, book closed
INSTRUMENT_STATE_HALTED6Trading halted, no orders or cancels
INSTRUMENT_STATE_MATCH_AND_CLOSE_AUCTION7Closing auction, will match upon state change
from polymarket.v1 import refdata_pb2

# Get state name
state_name = refdata_pb2.InstrumentState.Name(update.state)
print(f"State: {state_name}")

Complete Example (from stream.py)

This example matches the implementation from the Python examples repository:
import grpc
import requests
from datetime import datetime, timedelta
from typing import Optional
from polymarket.v1 import marketdatasubscription_pb2
from polymarket.v1 import marketdatasubscription_pb2_grpc
from polymarket.v1 import refdata_pb2


class PolymarketStreamer:
    def __init__(self, base_url: str = "https://rest.preprod.polymarketexchange.com",
                 grpc_server: str = "grpc-api.preprod.polymarketexchange.com:443"):
        self.base_url = base_url
        self.grpc_server = grpc_server
        self.access_token: Optional[str] = None
        self.refresh_token: Optional[str] = None
        self.access_expiration: Optional[datetime] = None
        self.price_scales: dict = {}  # symbol -> price_scale cache

    def get_price_scale(self, symbol: str) -> int:
        """Get price_scale for symbol from cache. Populate via list_instruments API."""
        # WARNING: Replace this with actual API lookup. Do not rely on default.
        return self.price_scales.get(symbol, 1000)

    def login(self, auth0_domain: str, client_id: str, private_key_path: str, audience: str) -> dict:
        """Authenticate using Private Key JWT and store the access token."""
        import jwt
        import uuid
        from cryptography.hazmat.primitives import serialization

        # Load private key
        with open(private_key_path, 'rb') as f:
            private_key = serialization.load_pem_private_key(f.read(), password=None)

        # Create JWT assertion
        now = int(datetime.now().timestamp())
        claims = {
            "iss": client_id,
            "sub": client_id,
            "aud": f"https://{auth0_domain}/oauth/token",
            "iat": now,
            "exp": now + 300,
            "jti": str(uuid.uuid4()),
        }
        assertion = jwt.encode(claims, private_key, algorithm="RS256")

        # Exchange for access token
        response = requests.post(
            f"https://{auth0_domain}/oauth/token",
            json={
                "client_id": client_id,
                "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                "client_assertion": assertion,
                "audience": audience,
                "grant_type": "client_credentials"
            }
        )
        response.raise_for_status()

        token_data = response.json()
        self.access_token = token_data["access_token"]
        # Set expiration with 30-second buffer (tokens expire in 180 seconds)
        expires_in = token_data.get("expires_in", 180)
        self.access_expiration = datetime.now() + timedelta(seconds=expires_in - 30)

        return token_data

    def stream_market_data(self, symbols: list, unaggregated: bool = False,
                          depth: int = 10, snapshot_only: bool = False):
        """Stream market data for the given symbols using gRPC."""
        if not self.access_token:
            raise ValueError("Not authenticated. Please login first.")

        # Create credentials
        credentials = grpc.ssl_channel_credentials()

        # Create channel
        channel = grpc.secure_channel(self.grpc_server, credentials)

        # Create stub
        stub = marketdatasubscription_pb2_grpc.MarketDataSubscriptionAPIStub(channel)

        # Create request
        request = marketdatasubscription_pb2.CreateMarketDataSubscriptionRequest(
            symbols=symbols,
            unaggregated=unaggregated,
            depth=depth,
            snapshot_only=snapshot_only
        )

        # Set up metadata with authorization
        metadata = [
            ('authorization', f'Bearer {self.access_token}')
        ]

        try:
            print(f"Starting market data stream for symbols: {symbols}")
            print(f"Parameters: unaggregated={unaggregated}, depth={depth}, snapshot_only={snapshot_only}")
            print("-" * 60)

            # Start streaming
            response_stream = stub.CreateMarketDataSubscription(request, metadata=metadata)

            for response in response_stream:
                self._process_market_data_response(response)

        except grpc.RpcError as e:
            print(f"gRPC error: {e.code()} - {e.details()}")
            raise
        except KeyboardInterrupt:
            print("\nStream interrupted by user")
        finally:
            channel.close()

    def _process_market_data_response(self, response):
        """Process and display market data response."""
        if response.HasField('heartbeat'):
            print(f"[{datetime.now().strftime('%H:%M:%S')}] Heartbeat received")

        elif response.HasField('update'):
            update = response.update
            print(f"\n[{datetime.now().strftime('%H:%M:%S')}] Market Update for {update.symbol}")

            # Display instrument state
            state_name = refdata_pb2.InstrumentState.Name(update.state)
            print(f"  State: {state_name}")

            # Get price_scale from instrument metadata (via list_instruments API)
            price_scale = self.get_price_scale(update.symbol)

            # Display order book
            if update.bids:
                print("  Bids:")
                for i, bid in enumerate(update.bids[:5]):  # Show top 5 bids
                    px = bid.px / price_scale  # Convert from price representation
                    qty = bid.qty
                    print(f"    [{i+1}] ${px:.4f} x {qty}")

            if update.offers:
                print("  Offers:")
                for i, offer in enumerate(update.offers[:5]):  # Show top 5 offers
                    px = offer.px / price_scale # Convert from price representation
                    qty = offer.qty
                    print(f"    [{i+1}] ${px:.4f} x {qty}")

            # Display stats if available
            if update.HasField('stats'):
                stats = update.stats
                print("  Stats:")
                if stats.HasField('last_trade_px'):
                    last_px = stats.last_trade_px / price_scale
                    print(f"    Last Trade: ${last_px:.4f}")
                if stats.HasField('open_px'):
                    open_px = stats.open_px / price_scale
                    print(f"    Open: ${open_px:.4f}")
                if stats.HasField('high_px'):
                    high_px = stats.high_px / price_scale
                    print(f"    High: ${high_px:.4f}")
                if stats.HasField('low_px'):
                    low_px = stats.low_px / price_scale
                    print(f"    Low: ${low_px:.4f}")
                if stats.HasField('shares_traded'):
                    print(f"    Shares Traded: {stats.shares_traded}")
                if stats.HasField('open_interest'):
                    print(f"    Open Interest: {stats.open_interest}")

            print("-" * 60)


# Usage
if __name__ == "__main__":
    streamer = PolymarketStreamer()

    # Login using Private Key JWT
    streamer.login(
        auth0_domain="auth.preprod.polymarketexchange.com",
        client_id="your_client_id",
        private_key_path="private_key.pem",
        audience="https://api.preprod.polymarketexchange.com"
    )

    # Stream market data
    streamer.stream_market_data(
        symbols=["tec-nfl-sbw-2026-02-08-kc"],
        depth=10
    )

Next Steps