Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Arena API keys API
version: 1.15.0
summary: 'Arena with an API key: read Arena-owned data, place paper orders with trade:write, and send live orders to your own venue account with a venue''s live-order scope.'
description: 'Arena Predictions (arena-predictions.com) is paper trading for prediction markets at live prices, in a sandbox, and live orders through the key owner''s own venue credentials. Paper is the default on every endpoint here (mode "paper"): a paper trade fills in Arena''s sandbox and is not sent to a venue. A live order is sent only when a request says execution kalshi or poly-intl with a key made with trade:write:kalshi or trade:write:poly-intl. It goes to the key owner''s own Kalshi or Polymarket International account with the venue credentials saved in Settings: real money at the venue, under the venue''s rules and terms. Arena is not a broker and never holds your funds. Polymarket International is not for US persons, and ProphetX orders are switched off (trade:write:prophetx is not issued, so execution prophetx is refused with 403 insufficient_scope; a key that carries the scope gets 503 trading_off). Keys read by default and reach Arena-owned data only: the key''s own identity and account, the leaderboards and published trader records with their resets and settled picks. Keys are managed with the signed-in browser session, never with a key. No exchange ticker, market title, entry price or venue price appears in a paper answer or a read while the venue-data switch is off; the owner''s own portfolio carries the owner''s own paper numbers (a resting order''s limitCents, a position''s costDollars and contracts). A live order''s answer names the venue''s own market and price (ticker, priceCents). Balances carry over: every account starts with 100,000 paper dollars, and a reset (at most one every 30 days, shown on a public profile) starts a new run. 1.2.0: seasons removed (no season field anywhere), leaderboard windows and kinds, trader resets, GET /account. 1.3.0: accounts are private by default. A private trader answers private: true with every P&L and rank null (resets and busts 0) and an empty pick list; a public trader whose earlier trades are still hidden has numbersShown: false and no P&L or rank. The key owner reads their own picks (each with placedPrivate and shown) and their own eval rows (GET /me/eval) with portfolio:read. No key can make an account public. 1.4.0: keys are managed by a person''s session, on the website or as a signed-in CLI''s session token (arena keys), never by a key; keys rotate with a grace period; every keyed answer carries the plan and its counts (RateLimit-*, Arena-Quota-*, Arena-Plan) and GET /usage says where the account stands; a new scope, markets:read, reads instruments, games and quotes; GET /portfolio and /portfolio/trades read the owner''s positions, orders and trades. Venue references and venue prices are served only while the venue-data switch is on (it ships off): until then the quotes routes answer 403 venue_data_off and nothing keyed names a ticker or a venue''s price. 1.5.0: GET /eval, the public eval board (records:read), each row with 95% intervals clustered by game. 1.6.0: GET /traders/{traderId}/track-record, a trader''s Ed25519-signed 90-day record exactly as Arena signed it (records:read; it names Kalshi series inside its signed bytes, so it is served only while the venue-data switch is on), and tiedWithAbove on each eval row (null while this row''s or the row above''s interval is unknown). 1.7.0: a key created with trade:write (only when asked for by name: the Settings checkbox, arena keys create --trade, or scopes in the body) places paper orders with POST /portfolio/orders and cancels them with DELETE /portfolio/orders/{orderId}, at Kalshi''s paths. Market and limit orders, buys by instrument or ticker and sells of a whole position, each with an idempotencyKey: the same key again answers the stored order with replayed: true and places nothing. Hard trade limits are always enforced: per key 10 new orders a minute and 50 a day, and 20 sells and cancels a minute; per account, across every key and connected app, 20 new orders a minute, 100 a day, and 40 sells and cancels a minute.'
termsOfService: https://arena-predictions.com/terms
contact:
name: Arena
email: support@zbgcllc.com
url: https://arena-predictions.com/support
servers:
- url: https://arena-predictions.com/api/v1
description: The Arena API. Paper trading, Arena's sandbox, is the default on every account. An order is sent to a venue only when the request says execution and the key carries that venue's scope.
tags:
- name: API Keys
description: 'Create, list, rotate and revoke keys, at the same path Kalshi uses. A person''s session only (the website, or a signed-in CLI''s session token): a key can never mint, rotate or revoke a key.'
paths:
/api_keys:
get:
tags:
- API Keys
operationId: listApiKeys
summary: List API keys
description: 'Every key on the signed-in account, newest first, including revoked, expired and rotating ones, each with this month''s requests, and the account''s plan. status=active keeps the keys that still work (active and rotating). Not paginated: an account holds at most 10 active keys.'
security:
- session: []
- sessionBearer: []
parameters:
- name: status
in: query
required: false
schema:
type: string
enum:
- all
- active
default: all
responses:
'200':
description: The account's keys.
content:
application/json:
schema:
$ref: '#/components/schemas/KeyListResponse'
'400':
description: 'invalid_request: status is not all or active.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 'unauthorized (not signed in) or session_required (an API key was presented: a key never manages keys).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: 'guest_account: the session is anonymous. Sign in with Google or Apple.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Could not load the keys.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags:
- API Keys
operationId: createApiKey
summary: Create API key
description: 'Mint a key. The plaintext token is returned once, in this response, and never again. Not idempotent: every call mints a new key. At most 20 keys (created or rotated) per account in any 24 hours.'
security:
- session: []
- sessionBearer: []
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/KeyCreateRequest'
responses:
'201':
description: Created.
content:
application/json:
schema:
$ref: '#/components/schemas/KeyCreateResponse'
'400':
description: 'invalid_request: a cookie-session request that is not same-origin (Sec-Fetch-Site must be same-origin, or Origin must equal this site), Content-Type is not application/json, the body is not an object, label is not a string, scopes is malformed or names an unknown scope, or expiresInDays is out of 1..365.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: unauthorized (not signed in) or session_required (an API key was presented).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: guest_account (anonymous session), or scope_not_available (trade:write requested while trading with keys is switched off).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 'key_limit_reached: ten active keys already. Revoke one first.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'key_creation_limited: 20 keys made in the last 24 hours. Retry-After says when the oldest turns a day old.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
headers:
Retry-After:
$ref: '#/components/headers/Retry-After'
'500':
description: Could not create the key.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api_keys/{id}:
delete:
tags:
- API Keys
operationId: revokeApiKey
summary: Revoke API key
description: 'Revoke one key (a rotating key too, which ends its grace). Takes effect on the next request that presents it. Safe to repeat: a second call answers 404 key_not_found, which means it is already gone.'
security:
- session: []
- sessionBearer: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: The key's id from the list.
responses:
'200':
description: Revoked.
content:
application/json:
schema:
$ref: '#/components/schemas/KeyRevokeResponse'
'400':
description: 'invalid_request: a cookie-session request that is not same-origin (Sec-Fetch-Site must be same-origin, or Origin must equal this site), or the id is not a uuid.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: unauthorized (not signed in) or session_required (an API key was presented).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: 'guest_account: the session is anonymous.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 'key_not_found: no active key with that id on this account (already revoked, or never yours).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Could not revoke the key.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api_keys/{id}/rotate:
post:
tags:
- API Keys
operationId: rotateApiKey
summary: Rotate API key
description: 'Replace a key: a new key with the same label and scopes (its token shown once in this response), and the old key kept working for graceSeconds (0 to 86400; 0 revokes it at once). One transaction. A rotating key does not count toward the 10-key cap; a rotation counts toward the 20 keys a day.'
security:
- session: []
- sessionBearer: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/KeyRotateRequest'
responses:
'201':
description: Rotated.
content:
application/json:
schema:
$ref: '#/components/schemas/KeyRotateResponse'
'400':
description: 'invalid_request: not same-origin (cookie session), not JSON, the id is not a uuid, graceSeconds is out of 0..86400, or expiresInDays is out of 1..365.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: unauthorized (not signed in) or session_required (an API key was presented).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: guest_account, or scope_not_available (the key carries trade:write while trading with keys is switched off).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 'key_not_found: not an active key of this account (revoked, expired, already rotated, or not yours).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 'key_creation_limited: 20 keys made in the last 24 hours.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
headers:
Retry-After:
$ref: '#/components/headers/Retry-After'
'500':
description: Could not rotate the key.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
KeyRevokeResponse:
type: object
required:
- key
- revoked
properties:
key:
$ref: '#/components/schemas/KeyView'
revoked:
type: boolean
const: true
KeyCreateResponse:
type: object
required:
- key
- token
- warning
properties:
key:
$ref: '#/components/schemas/KeyView'
token:
type: string
description: The plaintext key. Shown once; only its hash is stored.
warning:
type: string
KeyRotateRequest:
type: object
properties:
graceSeconds:
type: integer
minimum: 0
maximum: 86400
default: 0
description: How long the old key keeps working. 0 revokes it at once.
expiresInDays:
type: integer
minimum: 1
maximum: 365
description: 'The new key''s life. Omitted: the same life as the old key (at most 365 days), or none when the old key had none.'
Error:
type: object
required:
- error
properties:
error:
type: object
required:
- code
- message
properties:
code:
type: string
description: Stable machine token. Branch on this, never on message.
enum:
- invalid_request
- unauthorized
- invalid_token
- insufficient_scope
- guest_account
- scope_not_available
- key_limit_reached
- key_not_found
- trader_not_found
- account_not_found
- route_not_found
- method_not_allowed
- internal_error
- session_required
- key_creation_limited
- rate_limited
- quota_exceeded
- venue_data_off
- consolidated_book_off
- instrument_not_found
- game_not_found
- membership_required
- trading_off
- kalshi_key_missing
- kalshi_rejected
- poly_intl_key_missing
- poly_intl_rejected
- us_not_eligible
- prophetx_key_missing
- prophetx_rejected
- not_configured
- live_trading_unavailable
- idempotency_key_reused
- insufficient_balance
- market_not_found
- no_tradable_listing
- market_not_open
- market_resolved
- no_liquidity
- price_moved
- trade_not_found
- already_closed
- order_not_found
- organization_not_found
- organization_forbidden
message:
type: string
description: Human sentence. May change.
retryAfterSeconds:
type: integer
minimum: 1
description: 'On every 429: the same number as the Retry-After header.'
KeyCreateRequest:
type: object
properties:
label:
type: string
maxLength: 64
description: Trimmed; an empty label becomes "Untitled key".
scopes:
type: array
minItems: 1
items:
type: string
enum:
- records:read
- portfolio:read
- markets:read
- trade:write
- trade:write:kalshi
- trade:write:poly-intl
- trade:write:prophetx
description: 'Defaults to records:read, portfolio:read and markets:read. trade:write is never a default: name it to place and cancel paper orders. While trading with keys is switched off it is refused (403 scope_not_available). trade:write:kalshi and trade:write:poly-intl are never defaults either: name one to send orders to your own venue account with the venue credentials saved in Settings (each is refused with 403 scope_not_available while its venue''s orders are switched off). trade:write:prophetx is not issued: ProphetX orders are switched off.'
expiresInDays:
type: integer
minimum: 1
maximum: 365
description: Omit for a key that never expires.
KeyRotateResponse:
type: object
required:
- key
- token
- warning
- replaced
properties:
key:
$ref: '#/components/schemas/KeyView'
token:
type: string
description: The new plaintext key. Shown once; only its hash is stored.
warning:
type: string
replaced:
$ref: '#/components/schemas/KeyView'
KeyListResponse:
type: object
required:
- keys
- count
- usage
properties:
keys:
type: array
items:
$ref: '#/components/schemas/KeyView'
count:
type: integer
minimum: 0
usage:
type:
- object
- 'null'
description: The account's plan and this month's requests, or null when they could not be read.
properties:
tier:
type: string
enum:
- none
- basic
- builder
- desk
- enterprise
month:
type: object
properties:
limit:
type: integer
used:
type: integer
resetsAt:
type:
- string
- 'null'
format: date-time
minuteLimit:
type: integer
enforced:
type: boolean
KeyView:
type: object
required:
- id
- label
- tokenPrefix
- scopes
- status
- createdAt
- lastUsedAt
- expiresAt
- revokedAt
- createdVia
- rotatedFrom
- rotatedAt
- requestsThisMonth
properties:
id:
type: string
format: uuid
label:
type: string
maxLength: 64
tokenPrefix:
type: string
description: First 16 characters of the token, for matching a row to a key. Never the token.
scopes:
type: array
items:
type: string
enum:
- records:read
- portfolio:read
- markets:read
- trade:write
- trade:write:kalshi
- trade:write:poly-intl
- trade:write:prophetx
status:
type: string
enum:
- active
- rotating
- revoked
- expired
description: 'rotating: replaced by a rotation and still inside its grace period (it works until expiresAt).'
createdAt:
type: string
format: date-time
lastUsedAt:
type:
- string
- 'null'
format: date-time
description: Updated at most once an hour.
expiresAt:
type:
- string
- 'null'
format: date-time
revokedAt:
type:
- string
- 'null'
format: date-time
createdVia:
type: string
enum:
- web
- cli
description: 'web: Settings. cli: arena keys.'
rotatedFrom:
type:
- string
- 'null'
format: uuid
description: The key this one replaced by rotation.
rotatedAt:
type:
- string
- 'null'
format: date-time
description: When this key was rotated.
requestsThisMonth:
type: integer
minimum: 0
description: Keyed requests counted against this key this month (UTC).
headers:
Retry-After:
description: Seconds to wait before trying again.
schema:
type: integer
securitySchemes:
apiKey:
type: http
scheme: bearer
bearerFormat: arena_sk_ + 43 base64url characters
description: 'Authorization: Bearer arena_sk_... (the x-arena-key header is accepted too). Keys are made in Settings (https://arena-predictions.com/settings#api-keys) or with `arena keys create`, shown once, stored hashed.'
session:
type: apiKey
in: cookie
name: sb-access-token
description: The signed-in browser session (Google or Apple). Used only for key management, same-origin JSON requests only. An API key presented here is refused (401 session_required).
sessionBearer:
type: http
scheme: bearer
bearerFormat: Supabase access token (JWT) of a signed-in person
description: 'A signed-in person''s session token, as the CLI sends it after arena login. Key management checks it with the auth server (JSON bodies required; an arena_sk_ key there is 401 session_required). POST /backtest, POST /evals, GET /runs and GET /runs/{run_id} take it in place of a key and forward it to the backtest function, which checks it with the auth server. A bearer that starts with arena_sk_ is always read as a key. Never a guest session (403 guest_account) or a connected app''s OAuth token (401 session_required). Every other keyed route answers it 401 unauthorized: that route takes an API key.'
externalDocs:
description: Arena API docs
url: https://docs.arena-predictions.com