Prediction Markets
Prediction Markets on Gemini
Fetch live markets, stream prices, and place orders on event-based contracts via REST and WebSocket.
Make a Public REST Request
No API key required. Fetch live markets and copy an instrumentSymbol before writing a single line of trading code.
curl --request GET \
--url 'https://api.gemini.com/v1/prediction-markets/events?status=active&limit=3'Copy an instrumentSymbol from an active, open contract. That value is what you pass to WebSocket streams and order requests. All prices and quantities are decimal strings.
Events, Contracts, and Outcomes
An event is the question. A contract is one tradable outcome inside that event. Each contract has a YES side and a NO side.
Buy YES if you think the event happens, NO if you don't. Right pays $1.00, wrong pays $0.00. The price is what the market thinks the odds are.
Event: "Will BTC finish above $100,000 on Dec 31?"
Contract: BTC above $100,000
YES ask: $0.42
NO ask: $0.60Know Which Identifier to Use
API responses include several ticker fields. Use instrumentSymbol verbatim, exactly as returned, for all WebSocket and order requests.
Place a Maker-Only Limit Order
Use WebSocket for active trading. Authentication happens during the WebSocket handshake, so create an account-scoped key with time-based nonces enabled before connecting.
Before your first live order, create an API key and call POST /v1/prediction-markets/terms/accept. BTC 5-minute contracts expire every 5 minutes, so fetch a current instrumentSymbol from GET /v1/prediction-markets/events before sending this request. Buying 1 YES contract at $0.48 costs $0.48; the contract pays $1.00 if YES wins, $0.00 if it doesn't.
{
"id": "1",
"method": "order.place",
"params": {
"symbol": "GEMI-BTC05M2606011000-UP",
"side": "BUY",
"type": "LIMIT",
"timeInForce": "MOC",
"price": "0.48",
"quantity": "1",
"eventOutcome": "YES",
"clientOrderId": "btc-5m-quote-001"
}
}Gemini has a full sandbox environment at api.sandbox.gemini.com. It requires a separate account. The WebSocket endpoint is wss://ws.sandbox.gemini.com. Auth, endpoints, and message format are identical to production.
WebSocket Trading
Stream prices, place orders, and receive fills over a single persistent WebSocket connection. Authenticated streams require upgrade headers. Browsers cannot set these, so use a backend or local client.
Set up credentials
Create an account-scoped API key and export it as environment variables. WebSocket authentication requires account-scoped keys with time-based nonces enabled. Keys without these settings will be rejected at connection time.
export GEMINI_API_KEY="account-xxxxxxxxxxxxxxxx"
export GEMINI_API_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Fetch active market and stream prices
BTC 5-minute contracts expire every 5 minutes, so fetch the current symbol from REST before connecting. The code below resolves the active BTC05M contract, authenticates, and subscribes to {symbol}@bookTicker.
# pip install websockets
import asyncio, websockets, hmac, hashlib, base64, json, os, time, urllib.request, urllib.parse
from decimal import Decimal
API_KEY = os.environ["GEMINI_API_KEY"]
API_SECRET = os.environ["GEMINI_API_SECRET"]
def get_active_btc_5min_symbol() -> str:
url = ("https://api.gemini.com/v1/prediction-markets/events?"
+ urllib.parse.urlencode({"status": "active", "category": "crypto", "limit": "50"}))
with urllib.request.urlopen(url) as r:
data = json.loads(r.read())
for event in data.get("data", []):
for contract in event.get("contracts", []):
sym = contract.get("instrumentSymbol", "")
if ("BTC05M" in sym
and contract.get("status") == "active"
and contract.get("marketState") == "open"):
return sym
raise RuntimeError("No active BTC 5-minute contract found")
SYMBOL = get_active_btc_5min_symbol()
def auth_headers():
nonce = str(int(time.time() * 1000))
payload = base64.b64encode(nonce.encode())
signature = hmac.new(API_SECRET.encode(), payload, hashlib.sha384).hexdigest()
return {
"X-GEMINI-APIKEY": API_KEY,
"X-GEMINI-NONCE": nonce,
"X-GEMINI-PAYLOAD": payload.decode(),
"X-GEMINI-SIGNATURE": signature,
}
async def stream_prices():
async with websockets.connect("wss://ws.gemini.com", additional_headers=auth_headers()) as ws:
await ws.send(json.dumps({
"id": "1",
"method": "SUBSCRIBE",
"params": [f"{SYMBOL}@bookTicker"],
}))
async for msg in ws:
data = json.loads(msg)
if "b" in data and "a" in data:
print(f"{data['s']} bid: ${data['b']} ({data['B']} contracts) ask: ${data['a']} ({data['A']} contracts)")
asyncio.run(stream_prices())Subscribe and Place an Order
Accept Prediction Markets terms via REST before the first live order, then subscribe to orders@account and positions@account alongside the price stream. MOC rejects the order rather than crossing the spread. If it returns MakerOrCancelWouldTake, the price moved before the order landed.
async def trade():
async with websockets.connect("wss://ws.gemini.com", additional_headers=auth_headers()) as ws:
await ws.send(json.dumps({
"id": "1",
"method": "SUBSCRIBE",
"params": [
f"{SYMBOL}@bookTicker", # live prices
"orders@account", # your order updates
"positions@account", # your position updates
"contractStatus", # contract lifecycle events
],
}))
# Only place once; MOC rejects if we'd cross the spread.
async for msg in ws:
data = json.loads(msg)
if "b" in data and "a" in data and data.get("s") == SYMBOL:
best_bid = Decimal(data["b"])
best_ask = Decimal(data["a"])
price = min(best_bid, best_ask - Decimal("0.01"))
if price <= Decimal("0"):
continue
break
await ws.send(json.dumps({
"id": "2",
"method": "order.place",
"params": {
"symbol": SYMBOL,
"side": "BUY",
"type": "LIMIT",
"timeInForce": "MOC",
"price": str(price),
"quantity": "1",
"eventOutcome": "YES",
"clientOrderId": "btc-5m-quote-001",
},
}))Handle Order Updates and Cancel
Cancel resting quotes before intentionally closing the connection with order.cancel_session. On unexpected disconnect, reconnect and query orders@account for any open quotes before placing new ones.
# Watch order updates
# X = status, S = side, O = outcome, p = price, q = quantity
async for msg in ws:
data = json.loads(msg)
if data.get("X") in ("NEW", "OPEN", "FILLED", "PARTIALLY_FILLED", "CANCELED"):
print(f"Order {data['X']}: side={data.get('S')} outcome={data.get('O')} price=${data.get('p')} qty={data.get('q')}")
# In production, cancel only when your quoting logic decides the price is stale.
if data.get("X") == "OPEN":
await ws.send(json.dumps({
"id": "3",
"method": "order.cancel",
"params": { "orderId": data.get("i") },
}))
if data.get("X") == "CANCELED":
breakOrder NEW: side=BUY outcome=YES price=$0.48 qty=1
Order OPEN: side=BUY outcome=YES price=$0.48 qty=1
Order CANCELED: side=BUY outcome=YES price=$0.48 qty=1