Skip to content

dragonhuntr/go-polymarket-us-ws

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-polymarket-us-ws

A Go WebSocket client library for connecting to Polymarket US's WebSocket API.

Features

  • Ed25519 Authentication: Secure signature-based authentication using Ed25519
  • Type-Safe Handlers: Strongly-typed message handlers for compile-time safety
  • Auto-Reconnection: Automatic reconnection with exponential backoff
  • Subscription Management: Easy subscription tracking with request IDs
  • Two Endpoints: Support for both markets and private data streams

Installation

go get github.com/dragonhuntr/go-polymarket-us-ws

Endpoints

Markets WebSocket (/v1/ws/markets)

Real-time market data, order books, and trade notifications:

  • Market Data (Type 1): Full order book with statistics
  • Market Data Lite (Type 2): Lightweight price data
  • Trade (Type 3): Real-time trade notifications

Private WebSocket (/v1/ws/private)

User-specific data streams:

  • Orders (Type 1): Order updates (new, filled, canceled)
  • Positions (Type 3): Position changes
  • Account Balance (Type 4): Balance updates

Quick Start

1. Setup Credentials

package main

import (
    "log"
    "os"
    
    "github.com/dragonhuntr/go-polymarket-us-ws/pkg/auth"
    "github.com/dragonhuntr/go-polymarket-us-ws/pkg/ws"
)

func main() {
    // Parse Ed25519 private key from base64
    privateKey, err := auth.ParsePrivateKey(os.Getenv("PM_PRIVATE_KEY"))
    if err != nil {
        log.Fatal(err)
    }
    
    creds := &ws.Credentials{
        APIKeyID:   os.Getenv("PM_API_KEY_ID"),
        PrivateKey: privateKey,
    }

2. Create Markets Client

    // Create client for markets endpoint
    client, err := ws.NewClient(ws.EndpointMarkets, creds)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

3. Register Handlers

    // Register type-safe handlers
    client.OnMarketData(func(data *ws.MarketData) {
        log.Printf("[%s] State: %s, Bids: %d, Offers: %d", 
            data.MarketSlug, data.State, len(data.Bids), len(data.Offers))
        
        if len(data.Bids) > 0 && data.Bids[0].Px != nil {
            log.Printf("  Best Bid: %s %s", data.Bids[0].Px.Value, data.Bids[0].Px.Currency)
        }
        if len(data.Offers) > 0 && data.Offers[0].Px != nil {
            log.Printf("  Best Offer: %s %s", data.Offers[0].Px.Value, data.Offers[0].Px.Currency)
        }
    })
    
    client.OnTrade(func(trade *ws.Trade) {
        if trade.Price != nil && trade.Quantity != nil {
            log.Printf("[%s] Trade: %s @ %s %s", 
                trade.MarketSlug, 
                trade.Quantity.Value, 
                trade.Price.Value,
                trade.Price.Currency)
        }
    })
    
    client.OnError(func(reqID, errMsg string) {
        log.Printf("Error [%s]: %s", reqID, errMsg)
    })
    
    client.OnHeartbeat(func() {
        log.Println("Heartbeat received")
    })

4. Connect and Subscribe

    // Connect to WebSocket
    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }
    
    // Subscribe to full market data
    reqID, err := client.Subscribe(ws.MarketSubMarketData, []string{
        "presidential-election-2024",
        "bitcoin-price-prediction",
    })
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Subscribed to market data with request ID: %s", reqID)
    
    // Subscribe to trades with custom options
    tradeReqID, err := client.Subscribe(
        ws.MarketSubTrade,
        []string{"presidential-election-2024"},
        ws.WithRequestID("my-custom-trade-sub"),
        ws.WithDebounce(),
    )
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Subscribed to trades with request ID: %s", tradeReqID)
    
    // Block forever
    select {}
}

Examples

Private WebSocket - Order Updates

package main

import (
    "log"
    "os"
    "os/signal"
    
    "github.com/dragonhuntr/go-polymarket-us-ws/pkg/auth"
    "github.com/dragonhuntr/go-polymarket-us-ws/pkg/ws"
)

func main() {
    // Setup credentials
    privateKey, _ := auth.ParsePrivateKey(os.Getenv("PM_PRIVATE_KEY"))
    creds := &ws.Credentials{
        APIKeyID:   os.Getenv("PM_API_KEY_ID"),
        PrivateKey: privateKey,
    }
    
    // Create private endpoint client
    client, _ := ws.NewClient(ws.EndpointPrivate, creds)
    defer client.Close()
    
    // Register handlers
    client.OnOrderSnapshot(func(snapshot *ws.OrderSubscriptionSnapshot) {
        log.Printf("Order snapshot received: %d orders", len(snapshot.Orders))
        for _, order := range snapshot.Orders {
            priceStr := "N/A"
            if order.Price != nil {
                priceStr = order.Price.Value + " " + order.Price.Currency
            }
            log.Printf("  Order %s: %s %s @ %s (leaves: %.2f/%.2f, state: %s)",
                order.ID, order.MarketSlug,
                order.Side,
                priceStr,
                order.LeavesQuantity, order.Quantity,
                order.State)
        }
        if snapshot.EOF {
            log.Println("  [End of snapshot]")
        }
    })
    
    client.OnOrderUpdate(func(update *ws.OrderSubscriptionUpdate) {
        if update.Execution != nil {
            exec := update.Execution
            priceStr := "N/A"
            if exec.LastPx != nil {
                priceStr = exec.LastPx.Value + " " + exec.LastPx.Currency
            }
            log.Printf("Order execution: %s - %s shares @ %s (type: %s, trade: %s)",
                exec.ID, exec.LastShares, 
                priceStr,
                exec.Type,
                exec.TradeID)
        }
    })
    
    client.OnPosition(func(pos *ws.PositionSubscription) {
        log.Printf("Position update:")
        if pos.BeforePosition != nil {
            costStr := "N/A"
            if pos.BeforePosition.Cost != nil {
                costStr = pos.BeforePosition.Cost.Value + " " + pos.BeforePosition.Cost.Currency
            }
            log.Printf("  Before: %s @ %s",
                pos.BeforePosition.NetPosition,
                costStr)
        }
        if pos.AfterPosition != nil {
            costStr := "N/A"
            if pos.AfterPosition.Cost != nil {
                costStr = pos.AfterPosition.Cost.Value + " " + pos.AfterPosition.Cost.Currency
            }
            log.Printf("  After: %s @ %s",
                pos.AfterPosition.NetPosition,
                costStr)
        }
    })
    
    // Connect
    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }
    
    // Subscribe to orders (empty array = all markets)
    orderReqID, _ := client.Subscribe(ws.PrivateSubOrder, []string{})
    log.Printf("Subscribed to orders: %s", orderReqID)
    
    // Subscribe to positions for specific markets
    posReqID, _ := client.Subscribe(ws.PrivateSubPosition, []string{
        "presidential-election-2024",
    })
    log.Printf("Subscribed to positions: %s", posReqID)
    
    // Subscribe to account balance
    balReqID, _ := client.Subscribe(ws.PrivateSubAccountBalance, nil)
    log.Printf("Subscribed to balances: %s", balReqID)
    
    // Wait for interrupt signal
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt)
    <-sigCh
    
    log.Println("Shutting down...")
}

Market Data Lite - Lightweight Price Streaming

client, _ := ws.NewClient(ws.EndpointMarkets, creds)
defer client.Close()

client.OnMarketDataLite(func(data *ws.MarketDataLite) {
    log.Printf("[%s] Current: %s, Bid: %s, Ask: %s (Depth: %d/%d)",
        data.MarketSlug,
        priceStr(data.CurrentPx),
        priceStr(data.BestBid),
        priceStr(data.BestAsk),
        data.BidDepth,
        data.AskDepth)
})

client.Connect()

// Subscribe with debouncing for high-frequency updates
reqID, _ := client.Subscribe(
    ws.MarketSubMarketDataLite,
    []string{"high-volume-market"},
    ws.WithDebounce(),
)

select {}

func priceStr(p *ws.Price) string {
    if p == nil {
        return "N/A"
    }
    return p.Value + " " + p.Currency
}

API Reference

Client Creation

func NewClient(endpoint string, creds *Credentials) (*WSClient, error)

Parameters:

  • endpoint: Either ws.EndpointMarkets or ws.EndpointPrivate
  • creds: Credentials struct with API key ID and Ed25519 private key

Connection Management

func (c *WSClient) Connect() error
func (c *WSClient) IsConnected() bool
func (c *WSClient) Close() error

Subscription Management

func (c *WSClient) Subscribe(subType int, marketSlugs []string, opts ...SubscribeOption) (string, error)
func (c *WSClient) Unsubscribe(requestID string) error

Subscription Types (Markets):

  • ws.MarketSubMarketData (1) - Full order book
  • ws.MarketSubMarketDataLite (2) - Lightweight price data
  • ws.MarketSubTrade (3) - Trade notifications

Subscription Types (Private):

  • ws.PrivateSubOrder (1) - Order updates
  • ws.PrivateSubPosition (3) - Position updates
  • ws.PrivateSubAccountBalance (4) - Balance updates

Subscription Options:

  • ws.WithRequestID(id string) - Use custom request ID instead of auto-generated UUID
  • ws.WithDebounce() - Enable response debouncing for high-frequency markets

Handler Registration

Markets Handlers:

func (c *WSClient) OnMarketData(handler func(*MarketData))
func (c *WSClient) OnMarketDataLite(handler func(*MarketDataLite))
func (c *WSClient) OnTrade(handler func(*Trade))

Private Handlers:

func (c *WSClient) OnOrderSnapshot(handler func(*OrderSubscriptionSnapshot))
func (c *WSClient) OnOrderUpdate(handler func(*OrderSubscriptionUpdate))
func (c *WSClient) OnPosition(handler func(*PositionSubscription))
func (c *WSClient) OnBalanceSnapshot(handler func(*AccountBalancesSnapshot))
func (c *WSClient) OnBalanceUpdate(handler func(*AccountBalancesUpdate))

General Handlers:

func (c *WSClient) OnError(handler func(requestID, errMsg string))
func (c *WSClient) OnHeartbeat(handler func())

Message Structures

Common Types

type Price struct {
    Value    string // Decimal string (e.g., "0.54")
    Currency string // Currency code (e.g., "USD")
}

type OrderBookLevel struct {
    Px  *Price // Price level (pointer, may be nil)
    Qty string // Total quantity at this price
}

type MarketMetadata struct {
    Slug      string
    Icon      string
    Title     string
    Outcome   string
    EventSlug string
}

Market Data

type MarketData struct {
    MarketSlug   string
    Bids         []OrderBookLevel // Sorted highest to lowest
    Offers       []OrderBookLevel // Sorted lowest to highest
    State        string           // Market state string (e.g., "MARKET_STATE_OPEN")
    Stats        *MarketStats
    TransactTime string           // ISO 8601 timestamp
}

type MarketStats struct {
    LastTradePx  *Price
    SharesTraded string
    OpenInterest string
    HighPx       *Price
    LowPx        *Price
}

type MarketDataLite struct {
    MarketSlug   string
    CurrentPx    *Price
    LastTradePx  *Price
    BestBid      *Price
    BestAsk      *Price
    BidDepth     int
    AskDepth     int
    SharesTraded string
    OpenInterest string
}

type Trade struct {
    MarketSlug string
    Price      *Price
    Quantity   *Price
    TradeTime  string
    Maker      *TradeSide
    Taker      *TradeSide
}

type TradeSide struct {
    Side   string // e.g., "ORDER_SIDE_BUY"
    Intent string // e.g., "ORDER_INTENT_BUY_LONG"
}

Positions

type Position struct {
    NetPosition    string
    QtyBought      string
    QtySold        string
    Cost           *Price
    Realized       *Price
    BodPosition    string
    Expired        bool
    UpdateTime     string
    CashValue      *Price
    QtyAvailable   string
    MarketMetadata *MarketMetadata
}

type PositionSubscription struct {
    BeforePosition *Position
    AfterPosition  *Position
    UpdateTime     string
    EntryType      string
    TradeID        string
}

Balances

type Balance struct {
    CurrentBalance     float64
    Currency           string
    BuyingPower        float64
    AssetNotional      float64
    AssetAvailable     float64
    PendingCredit      float64
    OpenOrders         float64
    UnsettledFunds     float64
    MarginRequirement  float64
    LastUpdated        string
    PendingWithdrawals []PendingWithdrawal
}

type PendingWithdrawal struct {
    ID           string
    Balance      float64
    Status       string
    CreationTime string
}

type BalanceChange struct {
    BeforeBalance *Balance
    AfterBalance  *Balance
    Description   string
    UpdateTime    string
    EntryType     string
}

Orders

type Order struct {
    ID             string
    MarketSlug     string
    Side           string          // "ORDER_SIDE_BUY" or "ORDER_SIDE_SELL"
    Type           string          // "ORDER_TYPE_LIMIT" or "ORDER_TYPE_MARKET"
    Price          *Price          // Limit price (nil for market orders)
    Quantity       float64
    CumQuantity    float64         // Cumulative filled quantity
    LeavesQuantity float64         // Remaining unfilled quantity
    TIF            string          // Time in force (e.g., "TIME_IN_FORCE_GOOD_TILL_CANCEL")
    GoodTillTime   string          // Expiration time for GTD orders
    Intent         string          // Order intent (e.g., "ORDER_INTENT_BUY_LONG")
    MarketMetadata *MarketMetadata // Market information
    State          string          // Order state (e.g., "ORDER_STATE_FILLED")
    AvgPx          *Price          // Average execution price
    InsertTime     string          // Order insertion timestamp
    CreateTime     string          // Order creation timestamp
}

type Execution struct {
    ID                string
    Order             *Order
    LastShares        string          // Shares in this execution
    LastPx            *Price           // Execution price
    Type              string           // Execution type (e.g., "EXECUTION_TYPE_FILL")
    Text              string           // Additional text information
    OrderRejectReason string          // Rejection reason if applicable
    TransactTime      string           // Transaction timestamp
    TradeID           string           // Trade identifier
    Aggressor         bool             // Whether this was an aggressive order
}

type MarketMetadata struct {
    Slug      string
    Icon      string
    Title     string
    Outcome   string
    EventSlug string
}

Constants

Market States (String - used in responses)

const (
    MarketStateStrOpen       = "MARKET_STATE_OPEN"
    MarketStateStrPreopen    = "MARKET_STATE_PREOPEN"
    MarketStateStrSuspended  = "MARKET_STATE_SUSPENDED"
    MarketStateStrHalted     = "MARKET_STATE_HALTED"
    MarketStateStrExpired    = "MARKET_STATE_EXPIRED"
    MarketStateStrTerminated = "MARKET_STATE_TERMINATED"
)

Order Sides (String - used in responses)

const (
    OrderSideBuy  = "ORDER_SIDE_BUY"
    OrderSideSell = "ORDER_SIDE_SELL"
)

Order Types (String - used in responses)

const (
    OrderTypeStrLimit  = "ORDER_TYPE_LIMIT"
    OrderTypeStrMarket = "ORDER_TYPE_MARKET"
)

Order Intents (String - used in responses)

const (
    OrderIntentBuyLong   = "ORDER_INTENT_BUY_LONG"
    OrderIntentSellLong  = "ORDER_INTENT_SELL_LONG"
    OrderIntentBuyShort  = "ORDER_INTENT_BUY_SHORT"
    OrderIntentSellShort = "ORDER_INTENT_SELL_SHORT"
)

Time In Force (String - used in responses)

const (
    TIFGoodTillCancel    = "TIME_IN_FORCE_GOOD_TILL_CANCEL"
    TIFGoodTillDate      = "TIME_IN_FORCE_GOOD_TILL_DATE"
    TIFImmediateOrCancel = "TIME_IN_FORCE_IMMEDIATE_OR_CANCEL"
    TIFFillOrKill        = "TIME_IN_FORCE_FILL_OR_KILL"
)

Order States (String - used in responses)

const (
    OrderStatePendingNew      = "ORDER_STATE_PENDING_NEW"
    OrderStatePartiallyFilled = "ORDER_STATE_PARTIALLY_FILLED"
    OrderStateFilled          = "ORDER_STATE_FILLED"
    OrderStateCanceled        = "ORDER_STATE_CANCELED"
    OrderStateRejected        = "ORDER_STATE_REJECTED"
    OrderStateExpired         = "ORDER_STATE_EXPIRED"
    OrderStatePendingCancel   = "ORDER_STATE_PENDING_CANCEL"
    OrderStatePendingReplace  = "ORDER_STATE_PENDING_REPLACE"
    OrderStatePendingRisk     = "ORDER_STATE_PENDING_RISK"
    OrderStateReplaced        = "ORDER_STATE_REPLACED"
)

Execution Types (String - used in responses)

const (
    ExecutionTypePartialFill = "EXECUTION_TYPE_PARTIAL_FILL"
    ExecutionTypeFill        = "EXECUTION_TYPE_FILL"
    ExecutionTypeCanceled    = "EXECUTION_TYPE_CANCELED"
    ExecutionTypeRejected    = "EXECUTION_TYPE_REJECTED"
    ExecutionTypeExpired     = "EXECUTION_TYPE_EXPIRED"
    ExecutionTypeReplace     = "EXECUTION_TYPE_REPLACE"
    ExecutionTypeDoneForDay  = "EXECUTION_TYPE_DONE_FOR_DAY"
)

Ledger Entry Types (String - used in responses)

const (
    LedgerEntryTypeOrderExecution = "LEDGER_ENTRY_TYPE_ORDER_EXECUTION"
    LedgerEntryTypeDeposit        = "LEDGER_ENTRY_TYPE_DEPOSIT"
    LedgerEntryTypeWithdrawal     = "LEDGER_ENTRY_TYPE_WITHDRAWAL"
    LedgerEntryTypeResolution     = "LEDGER_ENTRY_TYPE_RESOLUTION"
    LedgerEntryTypeCommission     = "LEDGER_ENTRY_TYPE_COMMISSION"
)

Note: Integer constants (e.g., MarketStateOpen = 1, SideBuy = 1) are also available for request types, but WebSocket responses use string constants.

Features

Automatic Reconnection

The client automatically reconnects on connection loss with exponential backoff:

  • Initial delay: 1 second
  • Maximum delay: 60 seconds
  • Backoff multiplier: 2x

All active subscriptions are automatically restored after reconnection.

Thread Safety

All operations are thread-safe:

  • Concurrent handler registration
  • Concurrent message handling
  • Thread-safe connection state management
  • Mutex-protected write operations

Graceful Shutdown

client.Close() // Waits up to 2 seconds for clean shutdown

Authentication

The library uses Ed25519 signature-based authentication. During WebSocket handshake, the following headers are included:

  • X-PM-Access-Key: Your API key ID
  • X-PM-Timestamp: Current timestamp in milliseconds
  • X-PM-Signature: Base64-encoded Ed25519 signature of timestamp + "GET" + path

Generating Keys

import "github.com/dragonhuntr/go-polymarket-us-ws/pkg/auth"

// Parse base64-encoded private key (64 bytes)
privateKey, err := auth.ParsePrivateKey("your-base64-encoded-key")

// Build authentication headers for a path
headers, err := auth.BuildAuthHeaders(apiKeyID, privateKey, "/v1/ws/markets")

Error Handling

Errors are reported through the OnError handler:

client.OnError(func(requestID, errMsg string) {
    log.Printf("Subscription error [%s]: %s", requestID, errMsg)
})

Limits

  • Subscription Limit: Maximum 100 markets per subscription
  • Read Limit: 10MB per message
  • Shutdown Timeout: 2 seconds for graceful shutdown

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages