Authentication
PolySimulator uses two auth methods, scoped to different jobs:| Method | Header | Use it for |
|---|---|---|
| API key (primary) | X-API-Key: ps_live_… | Everything bots touch: trading, market data, websockets, balance/positions/history reads |
| Supabase Bearer JWT (dashboard / one-time) | Authorization: Bearer … | Self-service surfaces the dashboard reads with your signed-in session: POST /v1/keys/bootstrap, key management (GET/POST/DELETE /v1/keys, /v1/keys/tiers, /v1/keys/ws-token), GET /v1/me, /v1/account/me/entitlements, and /v1/me/wallets/* |
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.Key Format
Keys follow a predictable pattern for easy identification:ps_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2
Each key has a visible prefix (first 16 chars) used for identification without exposing the full key:
| Value | |
|---|---|
| Full key | ps_live_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2 |
| Prefix | ps_live_a1b2c3d4 |
How It Works
When you send a request:- Your API key is SHA-256 hashed and looked up in the database
- The key’s
is_activeandexpires_atfields are validated - Rate limits are enforced based on your key’s tier
- The associated user account is loaded for trading operations
Permissions
Keys support granular permissions:| Permission | Grants Access To |
|---|---|
read | Market data, prices, balance, positions, order history |
trade | Place orders, cancel orders, cancel-all, batch orders |
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
Store keys in environment variables
Store keys in environment variables
Never hardcode API keys in source code. Use environment variables or a secrets manager.
Rotate keys for short-lived CI/CD deployments
Rotate keys for short-lived CI/CD deployments
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.Principle of least privilege
Principle of least privilege
Create separate keys for different bots:
- Data-only bot:
["read"]permission - Trading bot:
["read", "trade"]permission
Rotate keys regularly
Rotate keys regularly
Create a new key, update your bot, then revoke the old key:
Maximum 5 keys per user
Maximum 5 keys per user
The system enforces a limit of 5 active keys per user account.
Revoke unused keys to free up slots.
Error Responses
| Status Code | Meaning | Common Causes |
|---|---|---|
401 Unauthorized | Invalid, expired, or deactivated API key | Typo in key, key was revoked, key expired |
403 Forbidden | Key lacks required permission for the endpoint | Using a read-only key to place trades |
429 Too Many Requests | Rate limit exceeded | Too many requests per second/minute for your tier |
/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.
X-Polysim-Code:
| Code | HTTP | When |
|---|---|---|
MISSING_AUTH | 401 | No Authorization: Bearer … or X-API-Key (or POLY_API_KEY) on a route that requires one |
MISSING_API_KEY | 401 | X-API-Key / POLY_API_KEY is missing on a key-only route (e.g. trading, market data, account reads) |
INVALID_KEY | 401 | Key doesn’t exist or is revoked |
INVALID_TOKEN | 401 | Bearer JWT is malformed, missing sub, or fails signature verification |
KEY_EXPIRED | 401 | Key past its expires_at |
KEY_DEACTIVATED | 401 | Key was administratively disabled |
KEY_OWNER_NOT_FOUND | 401 | Underlying user record missing (rare) |
TOKEN_EXPIRED | 401 | Bearer JWT expired |
ACCESS_RESTRICTED | 403 | An already-issued key (or a Bearer JWT) is not on the API v1 allowlist. Returned on authenticated /v1/* requests for accounts outside the admin-managed allowlist, and for accounts flagged or under review. |
CLOSED_BETA | 403 | Key issuance is gated: POST /v1/keys / POST /v1/keys/bootstrap refuse callers who are not in an admitted closed-beta cohort. The default outcome while the beta is closed. Apply via the waitlist. |
API_PRO_COMING_SOON | 403 | Key issuance for a paying Pro / Pro+ account that doesn’t yet have a cohort grant — a billing-aware “rolling it out in cohorts” variant of the issuance gate. Only reached once self-serve issuance is enabled; until then a paying caller also sees CLOSED_BETA. |
INSUFFICIENT_PERMISSION | 403 | Key lacks trade / other scope |
RATE_LIMIT_EXCEEDED | 429 | Per-key or per-IP burst exceeded |
On
429 responses, check the Retry-After header for exact wait time in seconds.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 theps_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, callPOST /v1/keys/bootstrap directly:
Security boundary
- JWTs are verified using HS256 against the project’s Supabase
signing secret, with
audience="authenticated", signature, expiry, and thesubUUID 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 requireX-API-Keyand 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_ALLOWEDand the caller must usePOST /v1/keys(withX-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 theX-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:
api_allowlist table (admin tooling: POST /admin/api-keys/issue-beta
also pre-creates an enterprise-tier key with a fixed cutoff).
Allowlist additions take effect on the next request — there’s no
client-visible toggle the gate is bound to.
Apply via the waitlist at
polysimulator.com/api-trading.
Beta-issued keys auto-downgrade after the cutoff
Beta-issued keys carry a fixedbeta_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: