Prop-Firm Quickstart
The full lifecycle your backend orchestrates: register a trader → send a login
link → create a $10,000 challenge → trader trades → pull the trade feed → close
or reset. Every step has copy-paste curl, Python, and TypeScript. All calls are
server-to-server with your tenant X-API-Key (Authentication);
the system model lives in Overview.
Set up your environment
Section titled “Set up your environment”export POLYSIM_TENANT_KEY="<your-tenant-key>"export POLYSIM_BASE_URL="https://api.polysimulator.com" # or your staging base URLimport os, requests
KEY = os.environ["POLYSIM_TENANT_KEY"]BASE = os.environ.get("POLYSIM_BASE_URL", "https://api.polysimulator.com")H = {"X-API-Key": KEY, "Content-Type": "application/json"}const KEY = process.env.POLYSIM_TENANT_KEY!;const BASE = process.env.POLYSIM_BASE_URL ?? "https://api.polysimulator.com";const H = { "X-API-Key": KEY, "Content-Type": "application/json" };
async function api<T>(method: string, path: string, body?: unknown): Promise<T> { const res = await fetch(`${BASE}${path}`, { method, headers: H, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(`${res.status} ${err.error ?? ""}: ${err.message ?? res.statusText}`); } return res.json() as Promise<T>;}-
Register the trader
Map your
external_id+ the trader’s email to a native PolySimulator user. Returns201on first registration,200on idempotent replay — either way you get auser_id.Terminal window curl -X POST "$POLYSIM_BASE_URL/v1/partner/users" \-H "X-API-Key: $POLYSIM_TENANT_KEY" \-H "Content-Type: application/json" \-d '{"external_id": "pc_trader_123","email": "trader@example.com","metadata": { "country": "ES" }}'r = requests.post(f"{BASE}/v1/partner/users", headers=H, json={"external_id": "pc_trader_123","email": "trader@example.com","metadata": {"country": "ES"},})r.raise_for_status()user = r.json()user_id = user["user_id"]print(f"trader user_id={user_id} created={user['created']}")const user = await api<{ user_id: number; created: boolean }>("POST", "/v1/partner/users",{ external_id: "pc_trader_123", email: "trader@example.com", metadata: { country: "ES" } },);const userId = user.user_id;console.log(`trader user_id=${userId} created=${user.created}`);Response (201) {"user_id": 12345,"tenant_user_id": "6a2f0c4e-1b7d-4e2a-9f3c-1a2b3c4d5e6f","external_id": "pc_trader_123","email": "trader@example.com","created": true} -
Send a magic-link login
Email the trader a one-time login link to sign in to the native PolySimulator UI. The link is never returned in the response.
Terminal window curl -X POST "$POLYSIM_BASE_URL/v1/partner/users/12345/login-link" \-H "X-API-Key: $POLYSIM_TENANT_KEY" \-H "Content-Type: application/json" \-d '{ "redirect_url": "https://polysimulator.com/markets" }'r = requests.post(f"{BASE}/v1/partner/users/{user_id}/login-link",headers=H,json={"redirect_url": "https://polysimulator.com/markets"},)r.raise_for_status() # 202 Acceptedprint("magic link dispatched")await api("POST", `/v1/partner/users/${userId}/login-link`, {redirect_url: "https://polysimulator.com/markets",});console.log("magic link dispatched");Response (202) { "status": "accepted" } -
Create a $10,000 challenge account
Provision an account funded to
starting_balance. Pick astage(demo/challenge/funded) and anexternal_idfor the account.Terminal window curl -X POST "$POLYSIM_BASE_URL/v1/partner/accounts" \-H "X-API-Key: $POLYSIM_TENANT_KEY" \-H "Content-Type: application/json" \-d '{"user_id": 12345,"external_id": "pc_challenge_001","stage": "challenge","starting_balance": "10000.00","metadata": { "plan": "10k", "attempt": 1 },"rules": { "profit_target_pct": 20, "max_drawdown_pct": 10 }}'r = requests.post(f"{BASE}/v1/partner/accounts", headers=H, json={"user_id": user_id,"external_id": "pc_challenge_001","stage": "challenge","starting_balance": "10000.00","metadata": {"plan": "10k", "attempt": 1},"rules": {"profit_target_pct": 20, "max_drawdown_pct": 10},})r.raise_for_status()account = r.json()account_id = account["account_id"]print(f"account_id={account_id} wallet_id={account['wallet_id']}")const account = await api<{ account_id: string; wallet_id: number }>("POST", "/v1/partner/accounts",{user_id: userId,external_id: "pc_challenge_001",stage: "challenge",starting_balance: "10000.00",metadata: { plan: "10k", attempt: 1 },rules: { profit_target_pct: 20, max_drawdown_pct: 10 },},);const accountId = account.account_id;console.log(`account_id=${accountId} wallet_id=${account.wallet_id}`);Response (201) {"account_id": "0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d","wallet_id": 9911,"user_id": 12345,"external_id": "pc_challenge_001","stage": "challenge","status": "active","starting_balance": "10000.00","rules": { "profit_target_pct": 20, "max_drawdown_pct": 10 },"metadata": { "plan": "10k", "attempt": 1 }} -
The trader trades (native UI)
The trader clicks the link, lands in the PolySimulator UI signed in, selects the challenge account, and trades against live Polymarket prices. There is no server-side order-placement endpoint in v1 — trading is the native UI, and your backend reads the fills in the next step. Challenge trades never touch the trader’s personal balance or the public leaderboard.
-
Read live account state
Poll the account-state envelope for balance, equity, and open positions (marked to market). Use this for live dashboards and drawdown checks.
Terminal window curl -H "X-API-Key: $POLYSIM_TENANT_KEY" \"$POLYSIM_BASE_URL/v1/partner/accounts/0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d"state = requests.get(f"{BASE}/v1/partner/accounts/{account_id}", headers=H).json()print(f"equity={state['equity']} unrealized={state['unrealized_pnl']}")const state = await api<{ equity: string; unrealized_pnl: string }>("GET", `/v1/partner/accounts/${accountId}`,);console.log(`equity=${state.equity} unrealized=${state.unrealized_pnl}`);Response (200) {"account_id": "0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d","external_id": "pc_challenge_001","stage": "challenge","status": "active","balance": "9875.12","starting_balance": "10000.00","equity": "10142.42","unrealized_pnl": "267.30","realized_pnl": null,"open_positions": [ { "market_id": "0xabc...", "outcome": "Yes", "quantity": "100.0000", "avg_entry_price": "0.4100", "current_price": "0.4350", "market_value": "43.50", "unrealized_pnl": "2.50" } ],"rules": { "profit_target_pct": 20, "max_drawdown_pct": 10 },"metadata": { "plan": "10k", "attempt": 1 },"updated_at": "2026-06-04T14:10:08.055516+00:00"} -
Pull the trade feed (your primary data source)
Page through filled trades with keyset pagination — newest first. Follow
next_cursoruntil it’snull. Filter withside(BUY/SELL) ormarket_id;limitdefaults to 100, max 500.Terminal window # First pagecurl -H "X-API-Key: $POLYSIM_TENANT_KEY" \"$POLYSIM_BASE_URL/v1/partner/accounts/0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d/trades?limit=500"# Next page — pass the previous response's next_cursorcurl -H "X-API-Key: $POLYSIM_TENANT_KEY" \"$POLYSIM_BASE_URL/v1/partner/accounts/0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d/trades?limit=500&cursor=eyJmaWxsZWRf..."def all_trades(account_id):cursor, out = None, []while True:params = {"limit": 500, **({"cursor": cursor} if cursor else {})}page = requests.get(f"{BASE}/v1/partner/accounts/{account_id}/trades",headers=H, params=params,).json()out.extend(page["data"])cursor = page["next_cursor"]if not cursor: # null → no more pagesreturn outtrades = all_trades(account_id)print(f"{len(trades)} fills")async function allTrades(accountId: string) {let cursor: string | null = null;const out: any[] = [];do {const p = new URLSearchParams({ limit: "500" });if (cursor) p.set("cursor", cursor);const page = await api<{ data: any[]; next_cursor: string | null }>("GET", `/v1/partner/accounts/${accountId}/trades?${p}`,);out.push(...page.data);cursor = page.next_cursor; // null → no more pages} while (cursor);return out;}const trades = await allTrades(accountId);console.log(`${trades.length} fills`);Response (200) {"data": [{ "trade_id": 8881, "order_id": 8881, "market_id": "0xabc...", "side": "BUY", "outcome": "Yes", "price": "0.4100", "quantity": "10.0000", "notional": "4.10", "fee": "0.00", "filled_at": "2026-06-04T14:09:31.220148+00:00", "client_order_id": "trader-key", "realized_pnl": null }],"next_cursor": "eyJmaWxsZWRfYXQiOiIyMDI2LTA1LTMxVDEyOjMwOjAwKzAwOjAwIiwib3JkZXJfaWQiOjg4ODF9"} -
Close or reset the attempt
When the trader passes, fails, or restarts, close or reset the account. Both take no request body and return the full account-state envelope.
Close archives the account and freezes the wallet — non-destructive and idempotent (a second close is a
200no-op):Terminal window curl -X POST "$POLYSIM_BASE_URL/v1/partner/accounts/0b1d8e2a-.../close" \-H "X-API-Key: $POLYSIM_TENANT_KEY"Reset restarts the same account (same
account_id, sameexternal_id) to itsstarting_balance, re-activating it if closed and preserving full history:Terminal window curl -X POST "$POLYSIM_BASE_URL/v1/partner/accounts/0b1d8e2a-.../reset" \-H "X-API-Key: $POLYSIM_TENANT_KEY"Both cancel any open orders first; if some can’t be cancelled (over the bulk-cancel limit or a transient lock) they return
409 ORDERS_NOT_CANCELLED— retry to drain the rest. See Endpoint Reference for the exact semantics. To keep one account per attempt instead, create a fresh account with a newexternal_idand close the old one.
Putting it together
Section titled “Putting it together”A minimal end-to-end provisioning + reconciliation loop in Python:
import os, requests
KEY = os.environ["POLYSIM_TENANT_KEY"]BASE = os.environ.get("POLYSIM_BASE_URL", "https://api.polysimulator.com")H = {"X-API-Key": KEY, "Content-Type": "application/json"}
def register_trader(external_id, email): r = requests.post(f"{BASE}/v1/partner/users", headers=H, json={"external_id": external_id, "email": email}) r.raise_for_status() return r.json()["user_id"]
def send_login(user_id, redirect="https://polysimulator.com/markets"): requests.post(f"{BASE}/v1/partner/users/{user_id}/login-link", headers=H, json={"redirect_url": redirect}).raise_for_status()
def create_challenge(user_id, external_id, balance="10000.00"): r = requests.post(f"{BASE}/v1/partner/accounts", headers=H, json={ "user_id": user_id, "external_id": external_id, "stage": "challenge", "starting_balance": balance, }) r.raise_for_status() return r.json()["account_id"]
def evaluate(account_id): state = requests.get(f"{BASE}/v1/partner/accounts/{account_id}", headers=H).json() cursor, fills = None, [] while True: params = {"limit": 500, **({"cursor": cursor} if cursor else {})} page = requests.get(f"{BASE}/v1/partner/accounts/{account_id}/trades", headers=H, params=params).json() fills.extend(page["data"]) cursor = page["next_cursor"] if not cursor: break # Your rule engine runs here, on `state` + `fills`. return state, fills
if __name__ == "__main__": uid = register_trader("pc_trader_123", "trader@example.com") send_login(uid) acc = create_challenge(uid, "pc_challenge_001") # ... trader trades in the native UI ... state, fills = evaluate(acc) print(f"equity={state['equity']} fills={len(fills)}")Next Steps
Section titled “Next Steps”- Endpoint Reference — Every field, status code, and edge case
- Rate Limits & Errors — Limits, error envelope, idempotency
- PolyChallenge onboarding — Your environment + support contact