Docs / Bot Building
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.
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.
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)}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"]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}")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")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
})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, ...}withdraw scope.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)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)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.
The project includes a complete reference bot at scripts/bot-trader.py that demonstrates:
For strategy and theory, see the Market Makers doc. For endpoint details, see the API reference.