Complete walkthrough of the canonical reference bot — 787 lines covering the full lifecycle
The Price Action Bot is the canonical reference implementation for Turbine trading bots. It demonstrates every aspect of the bot lifecycle in 787 lines of production-ready Python.
This is the recommended starting point for anyone building a Turbine bot. Every other bot follows this structure — only the trading signal logic changes.
The Price Action Bot trades BTC 15-minute prediction markets using real-time price data from Pyth Network — the same oracle Turbine uses to resolve markets.
Oracle alignment. The bot’s trading signal comes from the same data source (Pyth) that determines winners. When BTC is $500 above the strike with 2 minutes left, Pyth data says “YES is winning” — and that’s exactly what the bot trades on.This is the simplest and most intuitive strategy for newcomers.
API credentials are automatically registered on first run:
def get_or_create_api_credentials(env_path: Path = None): """Get existing credentials or register new ones and save to .env.""" api_key_id = os.environ.get("TURBINE_API_KEY_ID") api_private_key = os.environ.get("TURBINE_API_PRIVATE_KEY") if api_key_id and api_private_key: return api_key_id, api_private_key # Register new credentials by signing a message with wallet credentials = TurbineClient.request_api_credentials( host=TURBINE_HOST, private_key=private_key, ) # Auto-save to .env _save_credentials_to_env(env_path, credentials["api_key_id"], credentials["api_private_key"]) return credentials["api_key_id"], credentials["api_private_key"]
Key insight: The user only needs to provide TURBINE_PRIVATE_KEY in .env. Everything else is automatic.
Critical infrastructure pattern. Before trading, the bot must approve the settlement contract to spend USDC.Turbine uses a gasless EIP-2612 permit via the relayer — no native gas tokens (ETH, MATIC) required.
def ensure_settlement_approved(self, settlement_address: str) -> None: """Ensure USDC is approved for the settlement contract.""" # Check if already approved in this session if settlement_address in self.approved_settlements: return # Check on-chain allowance via API current_allowance = self.client.get_usdc_allowance(spender=settlement_address) if current_allowance >= self.MAX_APPROVAL_THRESHOLD: # Half of max uint256 self.approved_settlements[settlement_address] = current_allowance return # Submit gasless max permit via API result = self.client.approve_usdc_for_settlement(settlement_address) tx_hash = result.get("tx_hash") # Returns dict, not raw string # Wait for confirmation by polling allowance for _ in range(30): allowance = self.client.get_usdc_allowance(spender=settlement_address) if allowance >= self.MAX_APPROVAL_THRESHOLD: print(f"Max USDC approval confirmed (gasless)") self.approved_settlements[settlement_address] = allowance break time.sleep(2)
Key details:
One-time max approval per settlement contract (not per market, not per order)
Submitted via API, not direct RPC
Returns a dict with tx_hash, not a raw transaction hash
Check allowance first to avoid redundant approvals
Critical infrastructure pattern. After submitting an order, the bot must verify its state:
async def execute_signal(self, state: AssetState, action: str, confidence: float): # Create and submit order order = self.client.create_limit_buy( market_id=state.market_id, outcome=outcome, price=price, size=shares, expiration=int(time.time()) + 600, settlement_address=state.settlement_address, ) result = self.client.post_order(order) # Wait 2 seconds for settlement await asyncio.sleep(2) # === ORDER VERIFICATION CHAIN === # 1. Check for failures failed_trades = self.client.get_failed_trades() if my_order_failed: print(f"Order FAILED: {reason}") return # 2. Check for pending on-chain pending_trades = self.client.get_pending_trades() if my_order_pending: state.pending_order_txs.add(tx_hash) return # 3. Check if immediately filled recent_trades = self.client.get_trades(market_id=state.market_id, limit=20) if my_order_filled: # Update position tracking usdc_spent = (trade.size * trade.price) / (1_000_000 * 1_000_000) state.position_usdc[state.market_id] += usdc_spent return # 4. Check if resting on orderbook open_orders = self.client.get_orders(trader=self.client.address, market_id=state.market_id) if my_order_open: state.active_orders[order_hash] = action
Why this matters: Without verification, the bot doesn’t know if orders failed, filled, or are still open. This causes double-spends, missed fills, and incorrect position tracking.
Every 15 minutes, a new market opens. The bot must detect this and transition:
async def monitor_market_transitions(self): """Background task that polls for new markets every 5 seconds.""" while self.running: for asset in self.assets: market_info = await self.get_active_market(asset) new_market_id, end_time, start_price = market_info if new_market_id != state.market_id: # Switch to new market await self.switch_to_new_market(state, new_market_id, start_price) # Stop trading when <60s remain time_remaining = end_time - int(time.time()) if time_remaining <= 60: state.market_expiring = True await asyncio.sleep(5)
async def switch_to_new_market(self, state: AssetState, new_market_id: str, start_price: int): """Switch to a new market and reset state.""" # Track old market for claiming if state.market_id and state.contract_address: state.traded_markets[state.market_id] = state.contract_address # Cancel all orders on old market await self.cancel_asset_orders(state) # Update to new market state.market_id = new_market_id state.strike_price = start_price state.active_orders.clear() state.market_expiring = False # Fetch settlement address and approve USDC state.settlement_address = fetch_settlement_address(new_market_id) self.ensure_settlement_approved(state.settlement_address)
Key insight: The bot must cancel old orders before trading the new market, otherwise orders linger on expired markets.
Background task that checks for resolved markets and claims via the gasless relayer:
async def claim_resolved_markets(self): """Background task to claim winnings every 120 seconds.""" while self.running: # Collect all traded markets across all assets all_traded = [] for state in self.asset_states.values(): for market_id, contract_address in state.traded_markets.items(): all_traded.append((market_id, contract_address, state)) # Check which are resolved resolved = [] for market_id, contract_address, state in all_traded: resolution = self.client.get_resolution(market_id) if resolution and resolution.resolved: resolved.append((market_id, contract_address, state)) if resolved: # Batch claim in one transaction market_addresses = [addr for _, addr, _ in resolved] result = self.client.batch_claim_winnings(market_addresses) tx_hash = result.get("txHash", result.get("tx_hash")) print(f"Claimed {len(resolved)} markets TX: {tx_hash}") # Remove claimed markets from tracking for market_id, _, state in resolved: del state.traded_markets[market_id] await asyncio.sleep(120)
Key details:
Batch claiming is more gas-efficient than claiming individually
Enforces 120-second delay (API rate limit is 15 seconds, but less frequent is fine)
Removes claimed markets from tracking to avoid re-claiming
The bot tracks positions in USDC spent, not shares held. This simplifies limit checks:
def get_position_usdc(self, state: AssetState, market_id: str) -> float: """Get current position in USDC for a market.""" return state.position_usdc.get(market_id, 0.0)def can_trade(self, state: AssetState, usdc_amount: float) -> bool: """Check if trade would exceed max position.""" current = self.get_position_usdc(state, state.market_id) return (current + usdc_amount) <= self.max_position_usdc
python examples/price_action_bot.py \ --order-size 5 \ # $5 per order --max-position 50 \ # $50 max per asset per market --assets BTC,ETH # Trade BTC and ETH only
TURBINE_PRIVATE_KEY=0x... # Required: wallet private keyTURBINE_API_KEY_ID=... # Auto-generated on first runTURBINE_API_PRIVATE_KEY=... # Auto-generated on first runCHAIN_ID=137 # 137 = Polygon mainnetTURBINE_HOST=https://api.turbinefi.comCLAIM_ONLY_MODE=false # Set to true to disable trading
You can adjust these constants to change bot behavior:
# Order sizingDEFAULT_ORDER_SIZE_USDC = 1.0 # $1 per orderDEFAULT_MAX_POSITION_USDC = 10.0 # $10 max per asset# Signal sensitivityPRICE_THRESHOLD_BPS = 10 # 0.1% threshold before taking actionMIN_CONFIDENCE = 0.6 # Minimum confidence to tradeMAX_CONFIDENCE = 0.9 # Cap confidence at 90%# TimingPRICE_POLL_SECONDS = 10 # How often to check prices
If allowance is low, re-run approval: client.approve_usdc_for_settlement(settlement_address)
Bot keeps trading the old market after rotation
Cause: Market monitor task not running or switch_to_new_market() not called.Fix:
Check monitor_market_transitions() is running in background
Ensure orders are cancelled before switching: await self.cancel_asset_orders(state)
Verify new market ID is updated: state.market_id = new_market_id
Fills not tracked correctly (double-counting)
Cause: Not using processed_trade_ids to deduplicate.Fix:
for trade in recent_trades: if trade.id in state.processed_trade_ids: continue # Already processed state.processed_trade_ids.add(trade.id) # ... update position
'No claimable positions found' error
Cause: This is normal — means no markets have resolved yet.Fix: Ignore this error. It’s expected when checking for claims on a schedule.