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 broader question. A contract is one tradable proposition expressed in YES space. Each contract supports YES and NO outcomes.
Buy YES if you think the contract proposition will resolve YES, or NO if you think it will resolve NO. The winning outcome pays $1.00 and the losing outcome pays $0.00. Price reflects the market's implied probability.
Event: "Where will BTC finish on Dec 31?"
Instrument: BTC above $100,000 (YES-space proposition)
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, read the current terms, and check GET /v1/prediction-markets/terms/status. If the latest terms are not accepted, 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 contract of the YES outcome at $0.48 costs $0.48; that outcome pays $1.00 if the proposition resolves YES and $0.00 if it does not.
{
"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 demo environment, also called the sandbox, at api.sandbox.gemini.com. It requires a separate account. The WebSocket endpoint is wss://api.sandbox.gemini.com. Confirm that the product and operation are supported in the demo environment before testing.
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 accepts the order request, then cancels it if it would take liquidity. If it returns MakerOrCancelWouldTake, the price moved before the order could rest.
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 is canceled if it would 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 the REST active-orders and order-history endpoints for any open or accepted 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