A Go WebSocket client library for connecting to Polymarket US's WebSocket API.
- 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
go get github.com/dragonhuntr/go-polymarket-us-wsReal-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
User-specific data streams:
- Orders (Type 1): Order updates (new, filled, canceled)
- Positions (Type 3): Position changes
- Account Balance (Type 4): Balance updates
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,
} // Create client for markets endpoint
client, err := ws.NewClient(ws.EndpointMarkets, creds)
if err != nil {
log.Fatal(err)
}
defer client.Close() // 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")
}) // 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 {}
}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...")
}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
}func NewClient(endpoint string, creds *Credentials) (*WSClient, error)Parameters:
endpoint: Eitherws.EndpointMarketsorws.EndpointPrivatecreds: Credentials struct with API key ID and Ed25519 private key
func (c *WSClient) Connect() error
func (c *WSClient) IsConnected() bool
func (c *WSClient) Close() errorfunc (c *WSClient) Subscribe(subType int, marketSlugs []string, opts ...SubscribeOption) (string, error)
func (c *WSClient) Unsubscribe(requestID string) errorSubscription Types (Markets):
ws.MarketSubMarketData(1) - Full order bookws.MarketSubMarketDataLite(2) - Lightweight price dataws.MarketSubTrade(3) - Trade notifications
Subscription Types (Private):
ws.PrivateSubOrder(1) - Order updatesws.PrivateSubPosition(3) - Position updatesws.PrivateSubAccountBalance(4) - Balance updates
Subscription Options:
ws.WithRequestID(id string)- Use custom request ID instead of auto-generated UUIDws.WithDebounce()- Enable response debouncing for high-frequency markets
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())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
}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"
}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
}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
}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
}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"
)const (
OrderSideBuy = "ORDER_SIDE_BUY"
OrderSideSell = "ORDER_SIDE_SELL"
)const (
OrderTypeStrLimit = "ORDER_TYPE_LIMIT"
OrderTypeStrMarket = "ORDER_TYPE_MARKET"
)const (
OrderIntentBuyLong = "ORDER_INTENT_BUY_LONG"
OrderIntentSellLong = "ORDER_INTENT_SELL_LONG"
OrderIntentBuyShort = "ORDER_INTENT_BUY_SHORT"
OrderIntentSellShort = "ORDER_INTENT_SELL_SHORT"
)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"
)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"
)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"
)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.
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.
All operations are thread-safe:
- Concurrent handler registration
- Concurrent message handling
- Thread-safe connection state management
- Mutex-protected write operations
client.Close() // Waits up to 2 seconds for clean shutdownThe library uses Ed25519 signature-based authentication. During WebSocket handshake, the following headers are included:
X-PM-Access-Key: Your API key IDX-PM-Timestamp: Current timestamp in millisecondsX-PM-Signature: Base64-encoded Ed25519 signature oftimestamp + "GET" + path
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")Errors are reported through the OnError handler:
client.OnError(func(requestID, errMsg string) {
log.Printf("Subscription error [%s]: %s", requestID, errMsg)
})- Subscription Limit: Maximum 100 markets per subscription
- Read Limit: 10MB per message
- Shutdown Timeout: 2 seconds for graceful shutdown