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 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 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. (Free-tier keys are still read-only for trading and can’t be created with trade — that’s enforced at creation, see 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 Polymarket-shape: a single error field holding a human-readable description when one is available. (For unhandled exception paths where no message was set, the body falls back to the short machine code — so always branch on the X-Polysim-Code response header for stable error handling, not on the body text.) The X-Polysim-Code response header carries a stable short code — domain-specific where the handler knows what went wrong (e.g. INVALID_KEY, INSUFFICIENT_PERMISSION, RATE_LIMIT_EXCEEDED, BOOK_UNAVAILABLE, VALIDATION_FAILED), or HTTP_<status> as a generic fallback (HTTP_400, HTTP_500). The X-Request-Id response header always echoes the request id for log/support correlation.
Common auth/permission codes — branch on X-Polysim-Code:
On 429 responses, check the Retry-After header for exact wait time in seconds.
Verbose body opt-in. Send X-Polysim-Verbose: true on the request to get the legacy verbose body shape — useful while writing or debugging an SDK:
The default single-field shape matches Polymarket’s own /clob error contract, so PM-shape SDK ports work without translation.

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.

Closed Beta

The API is in an ongoing closed beta: new API keys are issued to approved cohorts only. There are two distinct gates, with distinct error codes — branch on the X-Polysim-Code response header:

1. Key issuance — CLOSED_BETA / API_PRO_COMING_SOON

POST /v1/keys/bootstrap (first key) and POST /v1/keys (additional keys) refuse callers who aren’t in an admitted cohort. While the beta is closed (the default), every non-admin, non-cohort caller — free, waitlisted, or paying — gets 403 CLOSED_BETA: apply via the waitlist and we’ll email you when a cohort opens. API_PRO_COMING_SOON is a conditional variant of the same gate. Once self-serve issuance is enabled, the gate routes by subscription tier: free callers self-serve a read-only key, while a paying Pro / Pro+ caller without a cohort grant gets the billing-aware 403 API_PRO_COMING_SOON (“your subscription includes API access; it’s being rolled out in cohorts”) instead. Until that flag flips, paying callers also see CLOSED_BETA. Both are PM-shape envelopes. The default body is the single-field {"error": "<human message>"} form; the stable machine code lives in the X-Polysim-Code response header. (The feature_key / upgrade_url routing hints from the underlying error are carried in the body only on 402 upgrade responses, not on these 403s — read them from the verbose body if you need them, via X-Polysim-Verbose: true.)

2. Runtime access — ACCESS_RESTRICTED

Once a key is issued, authenticated /v1/* requests are additionally gated by an admin-managed allowlist. An account that isn’t on the allowlist (or is flagged / under review) gets 403 ACCESS_RESTRICTED on every authenticated request. Like every /v1/* error, the stable machine code is in the X-Polysim-Code response header:
Access is granted by our team once your waitlist application is approved; additions take effect on your next request. Apply via the waitlist at polysimulator.com/api-trading.

Beta-issued keys auto-downgrade after the cutoff

Beta-issued keys carry a fixed beta_until cutoff. After the cutoff, the key is auto-downgraded to free-tier limits and read-only permissions on every request. Every response on a downgraded key includes X-API-Beta-Cutoff: expired so SDKs can pivot to read-only mode without a separate round-trip:
The public cohort-status endpoint reports current capacity (no auth required, used by the pricing page):

Next Steps