Skip to main content

Authentication

PolySimulator uses two auth methods, scoped to different jobs: Most users only ever see the API key — the developer console at polysimulator.com/console handles the Bearer-JWT bootstrap with your signed-in Supabase session, so you click a button and copy the ps_live_… value. The documented first-key setup requires one browser sign-in. The bootstrap API accepts an existing session token; it does not issue one. See Headless and CI setups. 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 creation — POST /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 developer console at polysimulator.com/console. Sign in, click Create a free API key, and copy the ps_live_… value shown once. The dashboard handles the Supabase JWT exchange transparently. (Once you hold a key, the console’s button reads New key.)

Headless and CI setups

Your first key needs one browser sign-in. There is no published programmatic sign-in: POST /v1/keys/bootstrap takes a Supabase access token, and the sign-in that issues one (Google, an emailed code, or a password) runs in the web app. Sign in once from any device (a phone works), create a key in the developer console, and store it as your CI secret. After that, no browser is needed. Any active key for your account can create, list, rotate and revoke your keys (see Permissions). On the free plan a job can mint a short-lived key from the stored one and revoke it when it finishes:
  • tier defaults to your plan’s tier. A paid plan allows one live key per paid tier, so on Pro or Pro+ the mint above is refused with 400 TIER_KEY_LIMIT_EXCEEDED. Use the stored key directly, or replace it without downtime with POST /v1/keys/{id}/rotate.
  • An account holds at most 5 active keys (400 KEY_LIMIT_EXCEEDED). Revoke what each run mints.
  • Keys are per deployment: a key from the production console does not work on any other host.

Headless and CI setups

Use a browser once to obtain your first key. Sign in on a device you control, open the developer console, create a key, and save it in your secret store. Configure your client or CI job to read that secret and send it in X-API-Key. There is no documented device-code approval flow or browserless sign-in procedure in this API. Do not treat POST /v1/keys/bootstrap as a login endpoint. An email-send response alone does not authenticate an account. If you cannot complete browser sign-in, first-key setup is not available through the documented non-browser flow. Later key management can use the API. With a valid credential and the required account access, use POST /v1/keys, GET /v1/keys, POST /v1/keys/{id}/rotate, or DELETE /v1/keys/{id}. Account access checks, permissions, and key limits still apply. Creating another key does not grant a new tier or bypass an access denial. New keys are not automatically short-lived. A job that creates a key must revoke it when finished, including after a failed run. A finite access grant can restrict its lifetime separately. Store a replacement key before retiring the credential that your jobs still use. If no usable key remains, return to browser sign-in and manage your keys in the console. An account that still has key records receives 400 BOOTSTRAP_NOT_ALLOWED. Revocation deletes a key’s record. When no key records remain, bootstrap can run again with a valid session token, subject to account access checks and the rate limit.

Bootstrap with an existing session token

An advanced client that already holds a valid Supabase access token can call POST /v1/keys/bootstrap with Authorization: Bearer <supabase_access_token> and a JSON body such as {"name": "my-first-bot"}. A successful response has status 201 and contains raw_key, which is shown only once. This documents key issuance, not how to obtain a session token without a browser. Do not extract browser cookies or refresh tokens as a CI setup step. Do not use a service-role secret or an operator credential to impersonate the account. Use the session token’s actual expiry. This API does not extend it. Missing, invalid, or expired credentials return 401; access denials return 403. An account with existing keys must use key management instead. A 429 response requires waiting before another attempt.

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 defaults to 5 calls/hour and 1 call/minute per IP, in addition to the global IP rate limit. The bootstrap limiter allows requests if Redis fails, so it is not a fail-closed security boundary. Authentication and account access checks still apply. The issued key belongs to the verified JWT subject, not an account selected in the request body.
  • API keys are bearer credentials. Anyone who obtains one can act with its permissions while account access remains valid. Do not put keys or session tokens in URLs, source control, shell history, or logs. Revoke an exposed key. Test revocation with that key alone; a second valid credential can mask its rejection on routes that accept both.

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