A Python client for market makers to interact with the Turbine CLOB (Central Limit Order Book) prediction markets API.
We have a Claude skill published that can help you write a trading bot. Run this command to get started:
curl -sSL turbinefi.com/claude | bashRun your trading bot 24/7 in the cloud with Railway (free $5 credit for 30 days):
# After creating your bot, deploy it:
claude "/railway-deploy"Or use the deployment script directly:
bash scripts/deploy-railway.shPrerequisites:
- A Railway account (railway.com) — free $5 credit for 30 days
- A generated bot (run the Claude skill above first)
- Railway CLI is installed automatically if not found
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
- Gasless USDC approvals and claiming winnings via API relayer
Fully API-routed — no RPC URL, web3 dependency, or blockchain node access required. Everything goes through api.turbinefi.com.
- Installation
- Getting API Credentials
- Quick Start
- Architecture
- Authentication
- Order Management
- Market Data
- Claiming Winnings
- WebSocket Streaming
- Data Types
- API Reference
- Examples
- Development
pip install turbine-py-clientOr install from source:
git clone https://github.com/ojo-network/turbine-py-client.git
cd turbine-py-client
pip install -e .To trade on Turbine, you need:
- Wallet Private Key - Your Ethereum wallet's private key for signing orders
- API Key ID - Identifier for your API key
- API Private Key - Ed25519 private key for authenticating API requests
No RPC URL or native gas tokens are required. The SDK routes all operations through the Turbine API.
You can register for API credentials directly using your wallet:
from turbine_client import TurbineClient
# Request API credentials (only need to do this once!)
credentials = TurbineClient.request_api_credentials(
host="https://api.turbinefi.com",
private_key="your_wallet_private_key",
)
print(f"API Key ID: {credentials['api_key_id']}")
print(f"API Private Key: {credentials['api_private_key']}")
# SAVE THESE! The private key cannot be retrieved later.This will:
- Sign a message with your wallet to prove ownership
- Generate new Ed25519 API credentials linked to your wallet
- Return the credentials (save the private key - it's only shown once!)
from turbine_client import TurbineClient
from turbine_client.types import Outcome, Side
# Initialize with all credentials
client = TurbineClient(
host="https://api.turbinefi.com",
chain_id=137, # Polygon mainnet
private_key="your_wallet_private_key",
api_key_id="your_api_key_id",
api_private_key="your_api_private_key",
)
# Get the latest BTC quick market
market = client.get_quick_market("BTC")
print(f"Market: {market.question}")
# Place a limit buy order
order = client.create_limit_buy(
market_id=market.market_id,
outcome=Outcome.YES,
price=500000, # 50% (price scaled by 1e6)
size=1_000_000, # 1 share (6 decimals)
)
result = client.post_order(order)
print(f"Order submitted: {result['orderHash']}")
# Check orderbook
orderbook = client.get_orderbook(market.market_id)
print(f"Best bid: {orderbook.bids[0].price / 10000:.2f}%")
print(f"Best ask: {orderbook.asks[0].price / 10000:.2f}%")
# Cancel the order
client.cancel_order(
order_hash=result['orderHash'],
market_id=market.market_id,
side=Side.BUY,
)
client.close()from turbine_client import TurbineClient
# Public endpoints - no auth required
client = TurbineClient(
host="https://api.turbinefi.com",
chain_id=137,
)
# 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...")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
- Modular Authentication - Separate concerns for EIP-712 signing vs Bearer tokens
- Builder Pattern - OrderBuilder encapsulates order creation and signing
- Dataclass Types - Clean, serializable data structures with type hints
- Async WebSocket - Non-blocking real-time updates
Turbine uses two authentication mechanisms:
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
)Some endpoints require Ed25519 bearer tokens:
# Token format: base64url(payload).base64url(signature)
# Payload: {"kid": keyId, "ts": timestamp, "n": nonce}Taker orders (orders that fill immediately against resting orders) must have a total value of at least $1 USDC (size × price ≥ $1). Orders below this minimum will be rejected by the API.
Maker orders (resting limit orders that provide liquidity) are exempt and can be any size.
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)Prices are scaled by 1,000,000 (1e6):
500000= 50% (even odds)250000= 25%750000= 75%- Range: 1 to 999,999
- Create - Build order parameters
- Sign - EIP-712 signature via eth_account
- Submit - POST to
/api/v1/orders - Match - Engine matches against orderbook
- Settle - On-chain settlement
- Confirm - Position updated
# 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...")# All markets
markets = client.get_markets()
# Filter by chain
markets = client.get_markets(chain_id=137)
# Single market
market = client.get_market(market_id="0x...")# 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}")# 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}")# 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}")# 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}")After a market resolves, you can claim your winnings via the API's gasless relayer (no RPC or gas tokens required).
from turbine_client import TurbineClient
client = TurbineClient(
host="https://api.turbinefi.com",
chain_id=137,
private_key="your_wallet_private_key",
api_key_id="your_api_key_id",
api_private_key="your_api_private_key",
)
# Claim winnings from a resolved market
result = client.claim_winnings("0xMarketContractAddress...")
print(f"Transaction: {result['tx_hash']}")# Claim from multiple resolved markets in a single transaction
result = client.batch_claim_winnings([
"0xMarket1Address...",
"0xMarket2Address...",
"0xMarket3Address...",
])
print(f"Transaction: {result['txHash']}")# Single market
python examples/claim_winnings.py 0xMarketAddress
# Multiple markets
python examples/batch_claim_winnings.py 0xMarket1 0xMarket2 --chain 137# Check if a market is resolved
resolution = client.get_resolution(market_id="0x...")
print(f"Resolved: {resolution.resolved}")
print(f"Winning outcome: {'YES' if resolution.outcome == 0 else 'NO'}")import asyncio
from turbine_client import TurbineWSClient
async def main():
ws = TurbineWSClient(host="wss://api.turbinefi.com")
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())# 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
}
}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:
id: int
market_id: str
buyer: str
seller: str
price: int
size: int
outcome: int
timestamp: int
tx_hash: str
@dataclass
class Position:
id: int
market_id: str
user_address: str
yes_shares: int
no_shares: int
yes_cost: int
no_cost: int
yes_revenue: int
no_revenue: int
total_invested: int
total_cost: int
total_revenue: int
last_updated: int
@dataclass
class Market:
id: str
chain_id: int
contract_address: str
settlement_address: str
question: str
description: str
category: str
expiration: int
maker: str
resolved: bool
winning_outcome: int | None
volume: int
created_at: int
updated_at: int
@dataclass
class Resolution:
market_id: str
assertion_id: str
outcome: int
resolved: bool
timestamp: int| Method | Endpoint | Description |
|---|---|---|
get_markets() |
GET /api/v1/markets |
List all markets |
get_market(market_id) |
GET /api/v1/stats/{id} |
Get market stats |
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_resolution(market_id) |
GET /api/v1/resolution/{id} |
Get resolution status |
get_quick_market(asset) |
GET /api/v1/quick-markets/{asset} |
Get active quick market |
get_quick_market_history(asset) |
GET /api/v1/quick-markets/{asset}/history |
Get quick market history |
get_quick_market_price(asset) |
GET /api/v1/quick-markets/{asset}/price |
Get current asset price |
get_quick_market_price_history(asset) |
GET /api/v1/quick-markets/{asset}/price-history |
Get asset price history |
get_failed_trades() |
GET /api/v1/failed-trades |
Get failed trades |
get_pending_trades() |
GET /api/v1/pending-trades |
Get pending trades |
get_failed_claims() |
GET /api/v1/failed-claims |
Get failed claims |
get_pending_claims() |
GET /api/v1/pending-claims |
Get pending claims |
get_settlement_status(tx_hash) |
GET /api/v1/settlements/{hash} |
Get settlement status |
| 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 |
get_order(hash) |
GET /api/v1/orders/{hash} |
Get specific order |
cancel_order(hash) |
DELETE /api/v1/orders/{hash} |
Cancel order |
get_positions(market_id) |
GET /api/v1/positions/{market} |
Get positions for market |
get_user_positions(addr) |
GET /api/v1/users/{addr}/positions |
All user positions |
get_user_orders(addr) |
GET /api/v1/users/{addr}/orders |
All user orders |
get_user_activity(addr) |
GET /api/v1/users/{addr}/activity |
Trading activity |
get_user_stats() |
GET /api/v1/user-stats |
Authenticated user stats |
| Method | Endpoint | Description |
|---|---|---|
approve_ctf_for_settlement() |
POST /api/v1/relayer/ctf-approval |
Gasless CTF approval |
claim_winnings(market_addr) |
POST /api/v1/relayer/ctf-redemption |
Claim from single market |
batch_claim_winnings(addrs) |
POST /api/v1/relayer/batch-ctf-redemption |
Claim from multiple markets |
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)from turbine_client import TurbineClient
client = TurbineClient(
host="https://api.turbinefi.com",
chain_id=137,
api_key_id="...",
api_private_key="..."
)
# Get all positions
positions = client.get_user_positions(
address="0x...",
chain_id=137
)
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}")# Clone repo
git clone https://github.com/ojo-network/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]"pytest tests/mypy turbine_py_client/ruff check turbine_py_client/
ruff format turbine_py_client/| Chain | Chain ID |
|---|---|
| Polygon Mainnet | 137 |
| Avalanche Mainnet | 43114 |
| Base Sepolia | 84532 |
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}")- Fork the repository
- Create a feature branch
- Make your changes
- Run tests and linting
- Submit a pull request
MIT License - see LICENSE