Skip to main content

Authentication

PolySimulator uses two auth methods, scoped to different jobs: Most users only ever see the API key — the dashboard at polysimulator.com/api-keys handles the Bearer-JWT bootstrap with your signed-in Supabase session, so you click a button and copy the ps_live_… value. The Bearer-JWT API path exists for headless setups (CI, dev tooling) where there’s no browser session. The Polymarket-CLOB-compatible read endpoints (e.g. /v1/book, /v1/midpoint, /v1/spread, /v1/markets-by-token) are public and don’t require a key. For convenience, PolySimulator also accepts the single-value header aliases POLY_API_KEY, Poly-API-Key, and Authorization: Bearer ps_live_…, each carrying the whole ps_live_ key, on authenticated routes (X-API-Key takes precedence when several are sent).
These aliases are a deliberate PolySimulator simplification — not a literal match of Polymarket’s request shape. Real Polymarket L2 auth attaches five POLY_* headers per request — POLY_ADDRESS, POLY_SIGNATURE (an HMAC-SHA256 of the request), POLY_TIMESTAMP, POLY_API_KEY, POLY_PASSPHRASE — and py-clob-client / @polymarket/clob-client never send a bare POLY_API_KEY or an Authorization: Bearer <key> on their own. PolySimulator collapses all of that to one value (your ps_live_ key) and ignores HMAC signing because it’s a paper-trading backend. So porting a bot still means pointing the SDK’s host at PolySimulator and feeding it the ps_live_ key — the aliases just mean common HTTP clients that default to Authorization: Bearer … or send POLY_API_KEY aren’t rejected; they don’t make a real py-clob-client work unchanged.
Bearer is rejected on every trading and market-data endpoint. POST /v1/orders, POST /v1/order, POST /v1/clob/order, DELETE /v1/orders/{id}, GET /v1/markets*, GET /v1/book, GET /v1/midpoint*, the websocket connect URL, and GET /v1/account/{balance,positions,portfolio,history,equity} all require X-API-Key (or the POLY_API_KEY / Poly-API-Key alias). This keeps the surface short-lived JWTs can reach narrow and auditable — short-lived browser tokens cannot reach the trade engine.

Key Format

Keys follow a predictable pattern for easy identification:
Example: ps_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2 Each key has a visible prefix (first 16 chars) used for identification without exposing the full key:

How It Works

When you send a request:
  1. Your API key is SHA-256 hashed and looked up in the database
  2. The key’s is_active and expires_at fields are validated
  3. Rate limits are enforced based on your key’s tier
  4. The associated user account is loaded for trading operations

Permissions

Keys support granular permissions:
A key with only read permission cannot place trades. Create a key with ["read", "trade"] permissions for bot usage.
Key management is gated by auth, not by the trade scope. Creating, listing, renaming, rotating, and revoking keys (POST/GET/PATCH/DELETE /v1/keys, /v1/keys/{id}/rotate) only require a valid credential for your account — any active ps_live_ key (even a read-only one) or your dashboard Supabase JWT. You don’t need a trade-scoped key to manage keys. (Whether a free key can trade depends on when it was issued — on or after 2026-09-05 it can, earlier it is read-only; see Open Beta below.)

Security Best Practices

Never hardcode API keys in source code. Use environment variables or a secrets manager.
There is no expires_at field on key creationPOST /v1/keys and POST /v1/keys/bootstrap only accept name, tier, and permissions. (expires_at is set server-side: it appears on a rotated key’s old half during the 24h overlap window, and on beta-issued keys as their beta_until cutoff.) For short-lived deployments, rotate instead: POST /v1/keys/{id}/rotate mints a replacement and schedules the old key to expire after a 24h overlap, so you can roll a key without downtime and let the old one lapse on its own.
Create separate keys for different bots:
  • Data-only bot: ["read"] permission
  • Trading bot: ["read", "trade"] permission
Create a new key, update your bot, then revoke the old key:
The system enforces a limit of 5 active keys per user account. Revoke unused keys to free up slots.

Error Responses

All /v1/* errors return a structured envelope with a stable machine code in error and human-readable prose in message. Branch on error, never on message. The identical code is also carried in X-Polysim-Code for clients that centralize response handling around headers. Domain-specific codes include INVALID_KEY, INSUFFICIENT_PERMISSION, RATE_LIMIT_EXCEEDED, BOOK_UNAVAILABLE, and VALIDATION_FAILED; handlers without a domain code use HTTP_<status> (for example, HTTP_400 or HTTP_500). The X-Request-Id response header always echoes the request id for log/support correlation.
Common auth/permission codes — branch on the response body’s error:
On 429 responses, check the Retry-After header for exact wait time in seconds.

Venue outage — VENUE_UNAVAILABLE

PolySim fills your orders against Polymarket’s live order book. When Polymarket itself is down, that book stops moving — so an order that has to execute now could only be filled at a frozen price. Rather than invent a fill, we pause those order shapes for the duration of the outage:
  • Paused: market orders, and limit orders with time_in_force of IOC, FOK or FAK. They return 503 VENUE_UNAVAILABLE.
  • Still accepted: ordinary limit orders (GTC, GTD). They rest as usual and report hold_reason: "venue_outage" in GET /v1/orders until the venue recovers, at which point matching resumes on its own — you do not need to re-place them.
  • Unchanged: cancellation, listing, market data, and account reads. A resting order can still be cancelled during an outage, and time-bound (Up/Down) orders still expire at their window close exactly as they would otherwise.
retry_after is in the default body as well as the Retry-After header — this is one of two documented exceptions to the single-field envelope (the other is 402 UPGRADE_REQUIRED), because an outage can last hours and the back-off interval is the one thing every caller needs. Back off on retry_after rather than retrying immediately: this 503 is a deliberate halt, not a transient failure, and it clears when Polymarket does. The Python SDK raises a dedicated VenueUnavailableError (carrying .retry_after) and deliberately does not consume its retry budget on it:
Verbose body opt-in. Send X-Polysim-Verbose: true to add diagnostic details and an in-body request_id:

Bootstrap Flow (First-Time Setup)

The recommended path is the dashboard at polysimulator.com/api-keys. Sign in, click Create your first API key, and copy the ps_live_… value shown once. The dashboard handles the Supabase JWT exchange transparently.

Bootstrap from a script (headless / CI)

If you can’t open a browser and you have a Supabase access token in hand, call POST /v1/keys/bootstrap directly:

Security boundary

  • JWTs are verified using HS256 against the project’s Supabase signing secret, with audience="authenticated", signature, expiry, and the sub UUID all enforced server-side. Anon and service-role tokens are rejected.
  • Bearer is accepted on the dashboard surface only: POST /v1/keys/bootstrap, key management (GET/POST/DELETE /v1/keys, /v1/keys/tiers, /v1/keys/ws-token), GET /v1/me, GET /v1/account/me/entitlements, and /v1/me/wallets/* — the routes the signed-in dashboard reads. Trading (/v1/orders, /v1/order, /v1/clob/order), market data (/v1/markets*, /v1/book, /v1/midpoint*, etc.), the account-trading reads (/v1/account/{balance,positions,portfolio, history,equity}), and the websocket connect URL all require X-API-Key and reject Bearer with 401 — short-lived JWTs cannot reach the trade engine or the account ledger.
  • Bootstrap is idempotency-bounded: if the JWT subject already has any key, the endpoint returns 400 BOOTSTRAP_NOT_ALLOWED and the caller must use POST /v1/keys (with X-API-Key) for additional keys.
  • Bootstrap is rate-limited at 5 calls/hour and 1 call/minute per IP, on top of the global IP rate limit. Real users only bootstrap once per account; the limit caps abuse without breaking legitimate network-error retries.

Open Beta

The public API is in open beta. Anyone who can sign in can mint a key. The default path is self-serve: The cutoff is the key’s created_at (UTC). A free account whose paid grant has expired stays read-only regardless of the date, and admin is never self-issued at any tier. Free + trade is no longer rejected; 403 TIER_REQUIRES_UPGRADE is now returned only when a free caller asks for a paid tier, for admin, or for trade on an expired paid grant. Paying subscribers are not blocked by a waitlist or API_PRO_COMING_SOON on the default path. CLOSED_BETA and API_PRO_COMING_SOON remain valid X-Polysim-Code values for a residual emergency close or a leftover cohort grant, but they are not the default outcome. Branch on the header, not the body prose.

Residual runtime access — ACCESS_RESTRICTED

Authenticated /v1/* requests can still return 403 ACCESS_RESTRICTED if an already-issued key’s account is flagged or under review. The stable machine code appears in both the body and the X-Polysim-Code response header:
This is not the default open-beta signup path.

Legacy beta-issued keys

Older cohort keys may still carry a beta_until cutoff. After that cutoff the key is auto-downgraded to free-tier limits and read-only permissions. Responses on a downgraded key include X-API-Beta-Cutoff: expired so SDKs can pivot without a separate round-trip:
The public cohort-status endpoint still reports capacity (no auth required; used by the pricing page for residual cohort inventory):

Next Steps