Skip to main content

Python client for Turbine CLOB prediction markets API

Project description

Turbine Python Client

A Python client for market makers to interact with the Turbine CLOB (Central Limit Order Book) prediction markets API.

Overview

This client provides a clean, typed interface for:

  • Creating and signing EIP-712 orders
  • Submitting orders to the Turbine orderbook
  • Managing positions and tracking fills
  • Subscribing to real-time orderbook updates via WebSocket
  • Multi-chain support (Base Sepolia, Polygon, etc.)

Inspired by Polymarket's py-clob-client.


Table of Contents

  1. Installation
  2. Quick Start
  3. Architecture
  4. Authentication
  5. Order Management
  6. Market Data
  7. WebSocket Streaming
  8. Data Types
  9. API Reference
  10. Examples
  11. Development

Installation

pip install turbine-py-client

Or install from source:

git clone https://github.com/turbine/turbine-py-client.git
cd turbine-py-client
pip install -e .

Dependencies

  • eth-account>=0.13.0 - Ethereum account management and signing
  • eth-utils>=4.1.1 - Ethereum utilities (keccak, address validation)
  • httpx[http2]>=0.27.0 - HTTP client with HTTP/2 support
  • websockets>=12.0 - WebSocket client
  • pynacl>=1.5.0 - Ed25519 signing for bearer tokens
  • python-dotenv>=1.0.0 - Environment variable management

Quick Start

Read-Only Access (No Authentication)

from turbine_client import TurbineClient

# Public endpoints - no auth required
client = TurbineClient(
    host="https://api.turbine.markets",
    chain_id=84532  # Base Sepolia
)

# Get all markets
markets = client.get_markets()

# Get orderbook for a market
orderbook = client.get_orderbook(market_id="0x...")

# Get recent trades
trades = client.get_trades(market_id="0x...")

Trading with EIP-712 Signed Orders

from turbine_client import TurbineClient, OrderArgs, Side, Outcome

# Initialize with private key for order signing
client = TurbineClient(
    host="https://api.turbine.markets",
    chain_id=84532,
    private_key="0x...",  # Your wallet private key
)

# Create and sign an order
order_args = OrderArgs(
    market_id="0x1234...",
    side=Side.BUY,
    outcome=Outcome.YES,
    price=500000,      # 50% (price scaled by 1e6)
    size=1000000,      # 1 share (6 decimals)
    expiration=int(time.time()) + 3600,  # 1 hour from now
)

signed_order = client.create_order(order_args)
response = client.post_order(signed_order)
print(f"Order submitted: {response['orderHash']}")

With Bearer Token Authentication

from turbine_client import TurbineClient

# For authenticated endpoints (positions, user orders)
client = TurbineClient(
    host="https://api.turbine.markets",
    chain_id=84532,
    private_key="0x...",
    api_key_id="abc123...",           # Ed25519 key ID
    api_private_key="0x...",          # Ed25519 private key (hex)
)

# Get your positions
positions = client.get_positions(user_address="0x...")

# Get your open orders
orders = client.get_orders(trader="0x...")

Architecture

Project Structure

turbine_py_client/
├── __init__.py                 # Main exports
├── client.py                   # TurbineClient class
├── signer.py                   # EIP-712 order signing
├── auth.py                     # Bearer token generation (Ed25519)
├── config.py                   # Chain configurations
├── constants.py                # Constants (endpoints, chain IDs)
├── exceptions.py               # Custom exceptions
├── types.py                    # Data types and models
├── order_builder/
│   ├── __init__.py
│   ├── builder.py              # OrderBuilder class
│   └── helpers.py              # Price/size utilities
├── http/
│   ├── __init__.py
│   └── client.py               # HTTP request handling
├── ws/
│   ├── __init__.py
│   └── client.py               # WebSocket client
└── utils.py                    # Utility functions

Design Patterns

Following Polymarket's proven patterns:

  1. Modular Authentication - Separate concerns for EIP-712 signing vs Bearer tokens
  2. Builder Pattern - OrderBuilder encapsulates order creation and signing
  3. Dataclass Types - Clean, serializable data structures with type hints
  4. Async WebSocket - Non-blocking real-time updates

Authentication

Turbine uses two authentication mechanisms:

1. EIP-712 Order Signing (Required for Trading)

All orders must be signed using EIP-712 structured data. The client handles this automatically.

Domain Separator:

{
    "name": "Turbine",
    "version": "1",
    "chainId": 84532,  # Chain-specific
    "verifyingContract": "0x..."  # Settlement contract
}

Order Type:

Order(
    bytes32 marketId,
    address trader,
    uint8 side,        # 0=BUY, 1=SELL
    uint8 outcome,     # 0=YES, 1=NO
    uint256 price,
    uint256 size,
    uint256 nonce,
    uint256 expiration,
    address makerFeeRecipient
)

2. Bearer Token Authentication (For Private Endpoints)

Some endpoints require Ed25519 bearer tokens:

# Token format: base64url(payload).base64url(signature)
# Payload: {"kid": keyId, "ts": timestamp, "n": nonce}

Generate API Keys:

# Use Turbine CLI to generate keys
./turbine genkey --name "my-market-maker" --owner 0x...

Order Management

Creating Orders

from turbine_client import OrderArgs, Side, Outcome

# Basic limit order
order = OrderArgs(
    market_id="0x1234...",
    side=Side.BUY,
    outcome=Outcome.YES,
    price=500000,       # 50%
    size=10000000,      # 10 shares
    expiration=int(time.time()) + 86400,  # 24 hours
)

signed = client.create_order(order)
result = client.post_order(signed)

Price Representation

Prices are scaled by 1,000,000 (1e6):

  • 500000 = 50% (even odds)
  • 250000 = 25%
  • 750000 = 75%
  • Range: 1 to 999,999

Order Lifecycle

  1. Create - Build order parameters
  2. Sign - EIP-712 signature via eth_account
  3. Submit - POST to /api/v1/orders
  4. Match - Engine matches against orderbook
  5. Settle - On-chain settlement
  6. Confirm - Position updated

Canceling Orders

# Cancel single order
client.cancel_order(
    order_hash="0x...",
    market_id="0x...",
    side=Side.BUY
)

# Cancel all orders for a market
client.cancel_market_orders(market_id="0x...")

Market Data

Get Markets

# All markets
markets = client.get_markets()

# Filter by chain
markets = client.get_markets(chain_id=84532)

# Single market
market = client.get_market(market_id="0x...")

Get Orderbook

# Full orderbook
orderbook = client.get_orderbook(market_id="0x...")

# Filter by outcome
yes_book = client.get_orderbook(market_id="0x...", outcome=Outcome.YES)

# Access bids/asks
for bid in orderbook.bids:
    print(f"Bid: {bid.price} x {bid.size}")

Get Trades

# Recent trades (last 100)
trades = client.get_trades(market_id="0x...")

# Trade details
for trade in trades:
    print(f"{trade.timestamp}: {trade.price} x {trade.size}")

Get Statistics

# Market stats
stats = client.get_stats(market_id="0x...")
print(f"24h Volume: {stats.volume_24h}")
print(f"Last Price: {stats.last_price}")

# Platform stats
platform = client.get_platform_stats()
print(f"Total Markets: {platform.market_count}")

Quick Markets (15-minute BTC/ETH)

# Get active quick market
qm = client.get_quick_market(asset="BTC")
print(f"Strike: ${qm.start_price / 1e8}")
print(f"Expires: {qm.end_time}")

# Price feed
price = client.get_quick_market_price(asset="BTC")
print(f"Current BTC: ${price.price_usd}")

WebSocket Streaming

Basic Subscription

import asyncio
from turbine_client import TurbineWSClient

async def main():
    ws = TurbineWSClient(host="wss://api.turbine.markets")

    async with ws.connect() as stream:
        # Subscribe to market
        await stream.subscribe(market_id="0x...")

        async for message in stream:
            if message.type == "orderbook":
                print(f"Orderbook update: {len(message.data.bids)} bids")
            elif message.type == "trade":
                print(f"Trade: {message.data.price} x {message.data.size}")

asyncio.run(main())

Message Types

# Orderbook update
{
    "type": "orderbook",
    "marketId": "0x...",
    "data": {
        "bids": [{"price": 500000, "size": 10000}],
        "asks": [{"price": 510000, "size": 5000}],
        "lastUpdate": 1705000000
    }
}

# Trade execution
{
    "type": "trade",
    "marketId": "0x...",
    "data": {
        "price": 505000,
        "size": 5000,
        "outcome": 0,
        "side": 0,
        "maker": "0x...",
        "taker": "0x..."
    }
}

# Quick market update
{
    "type": "quick_market",
    "data": {
        "asset": "BTC",
        "marketId": "0x...",
        "startPrice": 95000000000,
        "resolved": false
    }
}

Data Types

Core Types

from dataclasses import dataclass
from enum import Enum, IntEnum

class Side(IntEnum):
    BUY = 0
    SELL = 1

class Outcome(IntEnum):
    YES = 0
    NO = 1

@dataclass
class OrderArgs:
    market_id: str
    side: Side
    outcome: Outcome
    price: int          # 0 to 1,000,000
    size: int           # 6 decimals
    expiration: int     # Unix timestamp
    nonce: int = 0      # Auto-generated if 0
    maker_fee_recipient: str = "0x0000000000000000000000000000000000000000"

@dataclass
class SignedOrder:
    market_id: str
    trader: str
    side: int
    outcome: int
    price: int
    size: int
    nonce: int
    expiration: int
    maker_fee_recipient: str
    signature: str
    order_hash: str

@dataclass
class OrderBookSnapshot:
    market_id: str
    bids: list[PriceLevel]
    asks: list[PriceLevel]
    last_update: int

@dataclass
class PriceLevel:
    price: int
    size: int

@dataclass
class Trade:
    market_id: str
    price: int
    size: int
    outcome: int
    side: int
    maker: str
    taker: str
    timestamp: int
    trade_hash: str

@dataclass
class Position:
    market_id: str
    user_address: str
    yes_shares: int
    no_shares: int
    invested: int
    last_trade_price: int

@dataclass
class Market:
    id: str
    question: str
    description: str
    category: str
    expiration_time: int
    contract_address: str
    chain_id: int
    resolved: bool
    winning_outcome: int | None

API Reference

Public Endpoints (No Auth)

Method Endpoint Description
get_markets() GET /api/v1/markets List all markets
get_orderbook(market_id) GET /api/v1/orderbook/{id} Get orderbook
get_trades(market_id) GET /api/v1/trades/{id} Get trade history
get_stats(market_id) GET /api/v1/stats/{id} Get market stats
get_platform_stats() GET /api/v1/platform/stats Get platform stats
get_holders(market_id) GET /api/v1/holders/{id} Get top holders
get_quick_market(asset) GET /api/v1/quick-markets/{asset} Get active quick market

Authenticated Endpoints (Bearer Token)

Method Endpoint Description
post_order(order) POST /api/v1/orders Submit signed order
get_orders(trader) GET /api/v1/orders Get user's open orders
cancel_order(hash) DELETE /api/v1/orders/{hash} Cancel order
get_positions(user) GET /api/v1/positions/{market} Get user position
get_user_positions(addr) GET /api/v1/users/{addr}/positions All positions
get_user_activity(addr) GET /api/v1/users/{addr}/activity Trading activity

Relayer Endpoints (Gasless)

Method Endpoint Description
ctf_approval(request) POST /api/v1/relayer/ctf-approval Gasless CTF approval
ctf_redemption(request) POST /api/v1/relayer/ctf-redemption Gasless redemption

Examples

Market Making Bot

import asyncio
from turbine_client import TurbineClient, TurbineWSClient, OrderArgs, Side, Outcome

class SimpleMarketMaker:
    def __init__(self, client: TurbineClient, market_id: str):
        self.client = client
        self.market_id = market_id
        self.spread = 20000  # 2% spread

    async def run(self):
        ws = TurbineWSClient(host=self.client.host)

        async with ws.connect() as stream:
            await stream.subscribe(market_id=self.market_id)

            async for msg in stream:
                if msg.type == "orderbook":
                    await self.update_quotes(msg.data)

    async def update_quotes(self, orderbook):
        # Cancel existing orders
        self.client.cancel_market_orders(self.market_id)

        # Calculate mid price
        best_bid = orderbook.bids[0].price if orderbook.bids else 400000
        best_ask = orderbook.asks[0].price if orderbook.asks else 600000
        mid = (best_bid + best_ask) // 2

        # Place new quotes
        buy_order = self.client.create_order(OrderArgs(
            market_id=self.market_id,
            side=Side.BUY,
            outcome=Outcome.YES,
            price=mid - self.spread // 2,
            size=1000000,
            expiration=int(time.time()) + 300,
        ))

        sell_order = self.client.create_order(OrderArgs(
            market_id=self.market_id,
            side=Side.SELL,
            outcome=Outcome.YES,
            price=mid + self.spread // 2,
            size=1000000,
            expiration=int(time.time()) + 300,
        ))

        self.client.post_order(buy_order)
        self.client.post_order(sell_order)

Position Monitoring

from turbine_client import TurbineClient

client = TurbineClient(
    host="https://api.turbine.markets",
    chain_id=84532,
    api_key_id="...",
    api_private_key="..."
)

# Get all positions
positions = client.get_user_positions(
    address="0x...",
    chain_id=84532
)

for pos in positions:
    market = client.get_market(pos.market_id)
    print(f"\n{market.question}")
    print(f"  YES: {pos.yes_shares / 1e6:.2f} shares")
    print(f"  NO:  {pos.no_shares / 1e6:.2f} shares")
    print(f"  Cost Basis: ${pos.invested / 1e6:.2f}")

Development

Setup

# Clone repo
git clone https://github.com/turbine/turbine-py-client.git
cd turbine-py-client

# Create virtual environment
python -m venv venv
source venv/bin/activate

# Install dev dependencies
pip install -e ".[dev]"

Running Tests

pytest tests/

Type Checking

mypy turbine_py_client/

Linting

ruff check turbine_py_client/
ruff format turbine_py_client/

Chain Configuration

Chain Chain ID Settlement Contract
Base Sepolia (Testnet) 84532 TBD
Polygon 137 TBD

Error Handling

from turbine_client.exceptions import (
    TurbineApiError,
    OrderValidationError,
    SignatureError,
    AuthenticationError,
)

try:
    client.post_order(signed_order)
except OrderValidationError as e:
    print(f"Invalid order: {e}")
except AuthenticationError as e:
    print(f"Auth failed: {e}")
except TurbineApiError as e:
    print(f"API error ({e.status_code}): {e.message}")

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Run tests and linting
  5. Submit a pull request

License

MIT License - see LICENSE


Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

turbine_py_client-0.1.0.tar.gz (38.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

turbine_py_client-0.1.0-py3-none-any.whl (30.4 kB view details)

Uploaded Python 3

File details

Details for the file turbine_py_client-0.1.0.tar.gz.

File metadata

  • Download URL: turbine_py_client-0.1.0.tar.gz
  • Upload date:
  • Size: 38.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for turbine_py_client-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3fea00b38f75ee1f481f5f19a1681b6eaad7242f2faac13537cf99bd36b4270c
MD5 e9d7f27cc96766b2259f65091b9ef3f8
BLAKE2b-256 29db3ac28071d132e1f1a706f094a6ed259387e723479c0247be2d15e0a8e215

See more details on using hashes here.

File details

Details for the file turbine_py_client-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for turbine_py_client-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bc95a6be357b15139d8a25fb1a26b2d1d549b2d0e5a2b8f5250c80729d6674c0
MD5 e8e722e3da66fd95302fba4871d72517
BLAKE2b-256 2936609c26d4461a5f682880a85fb2227a864bd05ae93df1c428421ae90b43b0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page