Docs / Bot Building

Bot Building Guide

A hands-on walkthrough for building a trading bot on Polis Exchange using Python. We cover authentication, reading market data, placing orders, minting and redeeming tokens, and connecting to WebSocket feeds. A complete reference bot lives at scripts/bot-trader.py in the project repository.

Prerequisites

  • Python 3.8+ (no external packages needed — we use the standard library)
  • A Polis Exchange account with KYC tier 1 verification
  • For unattended bots: an API key created in Settings

All amounts in the API are in micro-USDC: 1 USDC = 1,000,000 micro-USDC. Always convert before sending prices to the API.

Polis uses conditional tokens (BULL and BEAR), not perpetual contracts. Each BULL/BEAR pair mints for 2 USDC and redeems for 2 USDC. There is no leverage, no margin, and no liquidations — your maximum loss on any token is the price you paid for it.

Step 1 — API helper

Start with a thin wrapper around urllib that handles JSON encoding and the Authorization header:

import json, urllib.request, urllib.error API = "http://localhost:3001/v1" def api(method, path, token=None, body=None): url = f"{API}{path}" data = json.dumps(body).encode() if body else None req = urllib.request.Request(url, data=data, method=method) req.add_header("Content-Type", "application/json") if token: req.add_header("Authorization", f"Bearer {token}") try: with urllib.request.urlopen(req, timeout=10) as resp: raw = resp.read().decode() return json.loads(raw) if raw else {} except urllib.error.HTTPError as e: body_text = e.read().decode() return json.loads(body_text) if body_text else {"error": str(e)}

Step 2 — Authenticate

Log in with email and password to obtain a JWT. The token expires after 15 minutes — store the refresh token and call /auth/refresh when you get a 401.

login = api("POST", "/auth/login", body={ "email": "you@example.com", "password": "your-password", }) token = login["accessToken"] user_id = login["userId"] # Later, when the token expires: refreshed = api("POST", "/auth/refresh", body={ "refreshToken": login["refreshToken"], }) token = refreshed["accessToken"]
API keys. For unattended bots, create an API key in Settings and pass it as the Bearer token instead. API keys do not expire and support scoped permissions (trade, withdraw, read).

Step 3 — Read the market

Fetch the order book to see current bids and asks. Prices are in micro-USDC and represent the per-token price for a single BULL or BEAR token.

book = api("GET", "/markets/JCB/book") # Book shape: {"bids": [{"price": 99500000, "size": 10}, ...], # "asks": [{"price": 100500000, "size": 8}, ...]} best_bid = max(b["price"] for b in book["bids"]) best_ask = min(a["price"] for a in book["asks"]) mid_price = (best_bid + best_ask) / 2 print(f"Mid: ${mid_price / 1_000_000:.2f}") print(f"Spread: ${(best_ask - best_bid) / 1_000_000:.2f}")

Step 4 — Check your balances

Before trading, check your USDC balance and token holdings. Open orders lock USDC; available balance is what you can spend on new orders or minting.

acct = api("GET", "/trading/account", token=token) # {"balance": 1000000000000, "available": 800000000000, # "reserved": 200000000000} print(f"Available: ${acct['available'] / 1_000_000:,.2f}") print(f"Reserved: ${acct['reserved'] / 1_000_000:,.2f}") tokens = api("GET", "/tokens/balances", token=token) # [{"ticker": "JCB", "bull": 150, "bear": 0, "settled": false}, ...] for t in tokens: if t["bull"] or t["bear"]: print(f"{t['ticker']}: {t['bull']} BULL, {t['bear']} BEAR")

Step 5 — Place orders

Three order types are supported: limit (rests on the book), market (crosses immediately), and post_only (rejected if it would cross). Always convert prices to micro-USDC. You are buying or selling individual BULL or BEAR tokens — there are no contracts or notional values.

# Limit buy: 5 BULL tokens at $99.50 order = api("POST", "/trading/orders", token=token, body={ "ticker": "JCB", "side": "buy", "type": "limit", "size": 5, "price": round(99.50 * 1_000_000), # 99500000 micro-USDC }) print(order["status"]) # "open", "filled", or "partially_filled" # Market sell: immediately cross the book order = api("POST", "/trading/orders", token=token, body={ "ticker": "JCB", "side": "sell", "type": "market", "size": 3, # no price field for market orders })
Fees. Makers (limit / post-only orders that rest) pay 0%. Takers (market orders and crossing limit orders) pay 0.05% of notional value. The fee is deducted from your USDC balance on fill.

Step 6 — Mint & redeem tokens

Minting creates a BULL/BEAR pair for 2 USDC (a 10 bps fee applies). Redeeming burns a matched pair and returns 2 USDC. This lets you supply liquidity to one side of the book and recycle matched tokens back to USDC.

# Mint: pay 2 USDC per pair, receive 1 BULL + 1 BEAR result = api("POST", "/tokens/mint", token=token, body={ "ticker": "JCB", "pairs": 5, # mint 5 BULL + 5 BEAR for 10 USDC }) print(result) # {"minted": 5, "feePaid": 1000000, ...} # Redeem: burn matched pairs, get 2 USDC each result = api("POST", "/tokens/redeem", token=token, body={ "ticker": "JCB", "pairs": 3, # burn 3 BULL + 3 BEAR for 6 USDC }) print(result) # {"redeemed": 3, "credited": 6000000, ...}
KYC required. Both mint and redeem require KYC tier 1. No 2FA is needed for mint or redeem. For programmatic access, use an API key with the withdraw scope.

Step 7 — Cancel orders & free balance

Open orders lock USDC. To free capital for new quotes, cancel stale orders before re-quoting. This is critical for market-making bots that refresh frequently.

# Cancel a single order api("DELETE", f"/trading/orders/{order_id}", token=token) # Cancel all open orders (loop) orders = api("GET", "/trading/orders", token=token) for o in orders: api("DELETE", f"/trading/orders/{o['id']}", token=token)

Step 8 — A simple market-making loop

Put it all together: read the mid-price, quote a bid below and an ask above, then repeat. Every few cycles, cancel stale orders and redeem matched token pairs to keep your USDC balance available.

import time TICKER = "JCB" SPREAD = 0.50 # $0.50 each side SIZE = 10 REPLENISH_EVERY = 5 trade_count = 0 while True: # Read the book book = api("GET", f"/markets/{TICKER}/book") if not book.get("bids") or not book.get("asks"): time.sleep(2) continue mid = (max(b["price"] for b in book["bids"]) + min(a["price"] for a in book["asks"])) / 2 # Place bid + ask bid_price = round((mid / 1e6 - SPREAD) * 1_000_000) ask_price = round((mid / 1e6 + SPREAD) * 1_000_000) api("POST", "/trading/orders", token=token, body={ "ticker": TICKER, "side": "buy", "type": "limit", "size": SIZE, "price": bid_price, }) api("POST", "/trading/orders", token=token, body={ "ticker": TICKER, "side": "sell", "type": "limit", "size": SIZE, "price": ask_price, }) trade_count += 1 # Periodic cleanup if trade_count % REPLENISH_EVERY == 0: orders = api("GET", "/trading/orders", token=token) for o in orders: api("DELETE", f"/trading/orders/{o['id']}", token=token) # Redeem matched BULL+BEAR pairs back to USDC tokens = api("GET", "/tokens/balances", token=token) for t in tokens: if t["ticker"] != TICKER: continue pairs = min(t["bull"], t["bear"]) if pairs > 0: api("POST", "/tokens/redeem", token=token, body={ "ticker": TICKER, "pairs": pairs, }) time.sleep(3)

WebSocket feeds

For low-latency updates, connect to the WebSocket gateway. Public channels (trades, book, ticker) require no auth. Private channels (your orders, token balances) require the same JWT or API key.

See the API reference for the full list of channels and message formats. The reference bot at scripts/bot-trader.py uses REST polling — if you need sub-second reactivity, switch to WebSocket.

Reference bot

The project includes a complete reference bot at scripts/bot-trader.py that demonstrates:

  • Multi-account setup (register, KYC, deposit)
  • JWT authentication with automatic re-login on 401
  • Two-sided limit order placement with jittered spreads
  • Token management (cancel stale orders, redeem matched pairs, top up USDC)
  • Automatic book replenishment after fills
  • Balance monitoring and auto re-deposit

For strategy and theory, see the Market Makers doc. For endpoint details, see the API reference.