Arena HTTP API
Base URL and versions
Every path in the reference is relative to one base URL. There is no sandbox: the money is paper either way.
REST API
https://arena-predictions.com/api/v1HTTPS only. Requests over plain HTTP are redirected, and a key sent over HTTP should be revoked. There is one environment: every account is a paper account, so a key acts on your real Arena account and a trade placed through the CLI is a real paper trade on your record.
Versioning
The API is versioned in the path. Within v1 changes are additive: new endpoints and new optional fields, never a renamed or removed one. A breaking change ships as /api/v2, with v1 kept for at least 180 days and the retirement dated in the changelog.
Specifications
GET /status needs no key and says whether the API is up. GET /openapi.json is the OpenAPI 3.1 description of every live endpoint, and /llms.txt is the short version for a language model.
Authentication
Send your key as a bearer token. Keys are created here, signed in, and shown once.
The header
Authorization: Bearer arena_sk_…A key is arena_sk_ followed by 43 base64url characters (256 bits). x-arena-key: arena_sk_… is accepted as an alternative header for clients that cannot set Authorization. A missing or bad credential answers 401 with a WWW-Authenticate: Bearer challenge; a valid key with the wrong scope answers 403 insufficient_scope.
Key lifecycle
- Only the hash of a key is stored. The plaintext exists in one HTTP response, at creation; lose it and you revoke it and create another. The list shows the first 16 characters so you can tell your keys apart.
- An account holds at most 10 active keys. A key may carry an expiry (1 to 365 days) and is revoked instantly; the next request that presents it gets
401 invalid_token. - Key management is done with your signed-in browser session, never with a key. A leaked read-only key must not be able to mint itself a replacement and outlive its own revocation, so there is deliberately no key-from-key endpoint. Creating and revoking a key accept same-origin requests only: a script that sends your session cookie must also send
Origin: https://arena-predictions.com, and a create must beContent-Type: application/json. - There is no guest or anonymous access. Every key belongs to a Google or Apple account, and every trade and record belongs to the person who signed in.
Scopes
- records:read
- Public Arena records: the players' season board, trader records and settled picks. Checked by GET /leaderboard, /traders/{traderId} and /traders/{traderId}/picks.
- portfolio:read
- The key owner’s own paper balance, positions and settled history. Recorded on the key now; the endpoints that will check it are not shipped yet.
- trade:write
- Reserved. Not issued in v1, because placing trades from a third-party key waits on the abuse and rate-limit work. Use the CLI with your own login to trade.
| Scope | Grants |
|---|---|
| records:read | Public Arena records: the players' season board, trader records and settled picks. Checked by GET /leaderboard, /traders/{traderId} and /traders/{traderId}/picks. |
| portfolio:read | The key owner’s own paper balance, positions and settled history. Recorded on the key now; the endpoints that will check it are not shipped yet. |
| trade:write | Reserved. Not issued in v1, because placing trades from a third-party key waits on the abuse and rate-limit work. Use the CLI with your own login to trade. |
Create a key
Making your first request
Two calls: one that needs nothing, and one that tells you who you are.
Status, no key
curl -sS https://arena-predictions.com/api/v1/status{
"ok": true,
"version": "v1",
"mode": "paper",
"season": "2026-09",
"time": "2026-09-22T04:10:00Z"
}Who am I, with a key
curl -sS https://arena-predictions.com/api/v1/me \
-H "Authorization: Bearer arena_sk_your_key_here"{
"user": { "id": "0f4c…", "displayName": "sharp_joe" },
"key": {
"prefix": "arena_sk_4Fq1XyZ",
"scopes": ["records:read", "portfolio:read"]
},
"mode": "paper"
}GET /me needs a valid key but no particular scope. It is how an agent discovers whose account it is acting for and what it is allowed to do, before it tries anything that could fail.
The same question from the CLI
$ arena whoami
userId 4d2f8c1e-9b0a-4c6e-8f21-3a7b5d9e0c12
displayName sharp_joe
isPro true
balanceDollars $1,046.00
monthKey 2026-09Conventions
The rules every endpoint follows, so you can read one and know the rest.
Names
JSON fields are camelCase (displayName, expiresInDays). Query parameters are snake_case. Enum values are lowercase (active, revoked, paper). Resources are plural nouns; a user’s own data lives under /portfolio, and the paths under it are Kalshi’s.
Units are in the name
*Cents is an integer price in cents, 1 to 99, for the side named. *Dollars and dollars are paper dollars as JSON numbers rounded to two decimals (a trailing zero is not represented: 984.3 is $984.30). *Count is an integer. *Pct is a percentage as a number, not a fraction.
Prices
A contract pays $1 if it settles yes. Stake = contracts × cents ÷ 100. A market order fills now, buying at the live ask and selling at the live bid. A limit order fills only at its price or better, and otherwise rests. No partial fills either way: an order fills whole or not at all.
Times
Instants are RFC 3339 UTC strings at second precision ending in Z, named *At (createdAt). Seasons are calendar months in America/New_York, written YYYY-MM and named season or monthKey. The CLI’s candlestick timestamps are Unix seconds, named *Ts.
Identifiers
Keys, traders and trades are UUIDs. Market tickers are uppercase strings and are opaque: never parse a ticker to infer anything. Arena instrument ids (ins_ plus 12 characters) are permanent and opaque too; see the universal ticker.
Every keyed response
Says "mode": "paper", and answers 201 on create and 200 with the updated resource on revoke. Key management, GET /me and every error are Cache-Control: no-store; a successful records read is Cache-Control: private, max-age=30. Keyed endpoints send no CORS headers: a key belongs in a server, a CLI or an agent runner, never in browser code. There is no streaming under a key; poll.
Identify your client
Send x-arena-client: yourapp/1.2.0 (or a descriptive User-Agent). The CLI and the MCP server do. It is not enforced today; it is the hook for telling a client its version is too old instead of breaking it. Trades the CLI or the MCP server opens at once, and orders they place, keep the tag they were sent with (cli/0.3.0, mcp/0.3.0). A limit order that rests and fills later keeps the tag on the order, not on the trade the fill creates, and sells are not tagged. That tag is self-reported and not verified, and a tag that is not cli, mcp, web or ios plus a version is not kept.
Where Arena differs from Kalshi
- Field names are camelCase here and snake_case on Kalshi. Paths and query parameters are snake_case on both.
- Money is a JSON number in paper dollars here; Kalshi sends fixed-point dollar strings (
"0.5600"). Prices are integer cents here; Kalshi’s are dollar strings between 0 and 1. - Instants are always
*AtRFC 3339 strings here; Kalshi mixes_timestrings and_tsUnix seconds. - A key is a bearer token here; Kalshi signs each request with an RSA key. A retried trade returns the original trade here; Kalshi answers 409 on a duplicate
client_order_id.
Pagination
Cursor-based, and the same on every list endpoint that has one.
How it works
GET /leaderboard and GET /traders/{traderId}/picks take limit (an integer from 1 to 100, with a documented default) and cursor, and answer with the rows and nextCursor, which is null on the last page. Pass nextCursor back as cursor and loop until it is null. A malformed cursor is 400 invalid_request. Lists do not carry a total count. Kalshi's lists work the same way, with cursor in the body instead of nextCursor.
Whole collections
GET /api_keys is not paginated: an account holds at most 10 active keys. In the CLI every list carries totalCount instead.
Errors
One envelope on every failure, so you can branch on the code and show the message.
The envelope
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="arena", error="invalid_token", error_description="API key is invalid, revoked or expired."
Cache-Control: no-store
{
"error": {
"code": "invalid_token",
"message": "API key is invalid, revoked or expired."
}
}code is a stable machine token that will not be reworded. message is a human sentence and may change. Kalshi’s envelope is the same two fields at the top level; Arena nests them under error.
Codes
Status codes mean what HTTP says they mean. Each row says whether retrying can help and which exit code the CLI turns it into.
- 400 invalid_request
- Malformed body or parameter. The message names the field. Final; fix the request. CLI exit 6.
- 401 unauthorized
- No credential was presented. Carries WWW-Authenticate: Bearer with error="invalid_request". CLI exit 4.
- 401 invalid_token
- A key was presented but it is malformed, unknown, revoked or expired; one answer for all four, on purpose. Carries WWW-Authenticate: Bearer with error="invalid_token". CLI exit 4.
- 403 insufficient_scope
- Valid key, wrong scope. Create a new key with the scope enabled. CLI exit 6.
- 403 guest_account
- Key management needs a signed-in Google or Apple account; an anonymous browser session cannot hold a key. CLI exit 6.
- 403 scope_not_available
- The scope exists but is not being issued yet (trade:write). CLI exit 6.
- 404 key_not_found
- No active key with that id on this account: already revoked, or never yours. CLI exit 6.
- 404 trader_not_found
- No trader has that id or display name. Final. CLI exit 6.
- 404 route_not_found
- No such path under /api/v1. Check the reference; a planned endpoint is not a live one. CLI exit 6.
- 409 key_limit_reached
- Ten active keys already. Revoke one first. CLI exit 6.
- 429
- Too many requests. Back off, honour Retry-After when it is present, and retry. CLI exit 6, marked retryable.
- 500 · 503 internal_error
- Our fault, not yours. The key may be perfectly good. A 503 also answers when the key could not be verified or the records could not be read right now. Retry with backoff (503 carries Retry-After); if it persists, get in touch. CLI exit 7.
| Status · code | Meaning |
|---|---|
| 400 invalid_request | Malformed body or parameter. The message names the field. Final; fix the request. CLI exit 6. |
| 401 unauthorized | No credential was presented. Carries WWW-Authenticate: Bearer with error="invalid_request". CLI exit 4. |
| 401 invalid_token | A key was presented but it is malformed, unknown, revoked or expired; one answer for all four, on purpose. Carries WWW-Authenticate: Bearer with error="invalid_token". CLI exit 4. |
| 403 insufficient_scope | Valid key, wrong scope. Create a new key with the scope enabled. CLI exit 6. |
| 403 guest_account | Key management needs a signed-in Google or Apple account; an anonymous browser session cannot hold a key. CLI exit 6. |
| 403 scope_not_available | The scope exists but is not being issued yet (trade:write). CLI exit 6. |
| 404 key_not_found | No active key with that id on this account: already revoked, or never yours. CLI exit 6. |
| 404 trader_not_found | No trader has that id or display name. Final. CLI exit 6. |
| 404 route_not_found | No such path under /api/v1. Check the reference; a planned endpoint is not a live one. CLI exit 6. |
| 409 key_limit_reached | Ten active keys already. Revoke one first. CLI exit 6. |
| 429 | Too many requests. Back off, honour Retry-After when it is present, and retry. CLI exit 6, marked retryable. |
| 500 · 503 internal_error | Our fault, not yours. The key may be perfectly good. A 503 also answers when the key could not be verified or the records could not be read right now. Retry with backoff (503 carries Retry-After); if it persists, get in touch. CLI exit 7. |
Rate limits
No per-key limit is enforced yet. Please keep each key under 60 requests a minute.
Today
The keyed endpoints enforce no per-key limit yet. Please keep each key under 60 requests a minute, and use nextCursor rather than re-reading a whole list. Treat any 429 as retryable: honour Retry-After when it is present, otherwise back off exponentially from one second.
Before a limit applies
Any per-key limit will be announced in the changelog before it applies, with the headers that report it documented here first.
Client-side caps
The MCP server caps itself, as runaway protection for an agent in a loop: by default 60 reads and 10 trades a minute, and 50 trades a day. That cap is local to the server and separate from anything the API does. The CLI has no cap of its own.
Idempotency and retries
Which calls are safe to repeat, and which failures are worth retrying.
Safe to repeat
Every GET. DELETE /api_keys/{id}: the second call answers 404 key_not_found, which means the key is already gone. POST /api_keys is not idempotent: every call mints a new key.
Reads and key management
Retry a network failure, a timeout, a 429 or a 5xx, with backoff. A 4xx other than 429 is final; fix the request.
Trades
Through the CLI today: every logical attempt carries a fresh UUID idempotencyKey. The same key is reused only when the attempt got no answer at all (a transport failure), and the server then returns the original trade rather than filling twice. An answered failure, 4xx or 5xx, is never retried with the same key. A sell is never retried.
Status
Is the API up. Paths are relative to the base URL.
Get status
GET /status
Whether the API is up, which version this is, that it is paper trading, and the running season (an America/New_York calendar month). Reads nothing, so it reports the API rather than the database.
Authorization: None.
Kalshi equivalent: GET /exchange/status
curl -sS https://arena-predictions.com/api/v1/status{
"ok": true,
"version": "v1",
"mode": "paper",
"season": "2026-09",
"time": "2026-09-22T04:10:00Z"
}Errors
- 5xx
- The API is not up. Retry with backoff.
| Status · code | When |
|---|---|
| 5xx | The API is not up. Retry with backoff. |
Identity
Whose account a key acts for, and what it may do.
Get me
GET /me
Whose account the presented key acts for and which scopes it carries. Needs a valid key and no particular scope, so an agent can discover its own permissions before trying anything that could fail. displayName is null until the account has chosen one.
Authorization: API key, any scope.
Kalshi equivalent: none; Arena only.
curl -sS https://arena-predictions.com/api/v1/me \
-H "Authorization: Bearer arena_sk_your_key_here"{
"user": { "id": "0f4c…", "displayName": "sharp_joe" },
"key": {
"prefix": "arena_sk_4Fq1XyZ",
"scopes": ["records:read", "portfolio:read"]
},
"mode": "paper"
}Errors
- 401 unauthorized
- No key was sent.
- 401 invalid_token
- The key is malformed, unknown, revoked or expired.
- 503 internal_error
- The key could not be verified right now. Retry after the Retry-After seconds; the key may be perfectly good.
| Status · code | When |
|---|---|
| 401 unauthorized | No key was sent. |
| 401 invalid_token | The key is malformed, unknown, revoked or expired. |
| 503 internal_error | The key could not be verified right now. Retry after the Retry-After seconds; the key may be perfectly good. |
API keys
Create, list and revoke keys, at the same paths Kalshi uses. Browser session only: a key presented here is refused with 401.
In practice you use these through the panel in the Authentication section; they are documented so what the panel does is not a mystery.
List API keys
GET /api_keys
Every key on the account, newest first, including revoked and expired ones. status is computed server-side from revokedAt and expiresAt. Not paginated: an account holds at most 10 active keys.
Authorization: Browser session (Google or Apple).
Kalshi equivalent: GET /api_keys
curl -sS https://arena-predictions.com/api/v1/api_keys \
-H "Cookie: <your signed-in browser session>"{
"keys": [
{
"id": "8c0b6f1e-2a3d-4e5f-9a7b-1c2d3e4f5a6b",
"label": "Claude Code on the laptop",
"tokenPrefix": "arena_sk_4Fq1XyZ",
"scopes": ["records:read", "portfolio:read"],
"status": "active",
"createdAt": "2026-09-15T18:02:11Z",
"lastUsedAt": "2026-09-22T03:58:40Z",
"expiresAt": null,
"revokedAt": null
}
],
"count": 1
}Errors
- 401 unauthorized
- Not signed in.
- 403 guest_account
- The session is anonymous. Sign in with Google or Apple.
- 500 internal_error
- Could not load the keys. Retry.
| Status · code | When |
|---|---|
| 401 unauthorized | Not signed in. |
| 403 guest_account | The session is anonymous. Sign in with Google or Apple. |
| 500 internal_error | Could not load the keys. Retry. |
Create API key
POST /api_keys
Mint a key. The plaintext token is in this response and nowhere else, ever. Not idempotent: every call mints a new key.
Authorization: Browser session (Google or Apple).
Kalshi equivalent: POST /api_keys
Request body
- label
- string, optional. Trimmed and truncated to 64 characters; empty becomes “Untitled key”.
- scopes
- string[], optional. Defaults to records:read and portfolio:read. Unknown scopes are refused; trade:write is reserved.
- expiresInDays
- integer 1..365, optional. Omit for a key that never expires.
| Field | Type and meaning |
|---|---|
| label | string, optional. Trimmed and truncated to 64 characters; empty becomes “Untitled key”. |
| scopes | string[], optional. Defaults to records:read and portfolio:read. Unknown scopes are refused; trade:write is reserved. |
| expiresInDays | integer 1..365, optional. Omit for a key that never expires. |
curl -sS -X POST https://arena-predictions.com/api/v1/api_keys \
-H "Cookie: <your signed-in browser session>" \
-H "Origin: https://arena-predictions.com" \
-H "Content-Type: application/json" \
-d '{ "label": "Claude Code on the laptop", "scopes": ["records:read", "portfolio:read"], "expiresInDays": 90 }'{
"key": {
"id": "8c0b6f1e-2a3d-4e5f-9a7b-1c2d3e4f5a6b",
"label": "Claude Code on the laptop",
"tokenPrefix": "arena_sk_4Fq1XyZ",
"scopes": ["records:read", "portfolio:read"],
"status": "active",
"createdAt": "2026-09-22T04:10:00Z",
"lastUsedAt": null,
"expiresAt": "2026-12-21T04:10:00Z",
"revokedAt": null
},
"token": "arena_sk_4Fq1XyZ…(43 characters after the prefix)",
"warning": "Copy this key now. It is shown once and cannot be recovered, so if you lose it, revoke it and create another."
}Errors
- 400 invalid_request
- The body is not an object, label is not a string, scopes is malformed or names an unknown scope, or expiresInDays is outside 1..365.
- 401 unauthorized
- Not signed in.
- 403 guest_account
- The session is anonymous.
- 403 scope_not_available
- trade:write was requested. It is not issued yet.
- 409 key_limit_reached
- Ten active keys already. Revoke one first.
- 500 internal_error
- Could not create the key. Retry.
| Status · code | When |
|---|---|
| 400 invalid_request | The body is not an object, label is not a string, scopes is malformed or names an unknown scope, or expiresInDays is outside 1..365. |
| 401 unauthorized | Not signed in. |
| 403 guest_account | The session is anonymous. |
| 403 scope_not_available | trade:write was requested. It is not issued yet. |
| 409 key_limit_reached | Ten active keys already. Revoke one first. |
| 500 internal_error | Could not create the key. Retry. |
Revoke API key
DELETE /api_keys/{id}
Revoke one key. It takes effect on the next request that presents the key. The row stays in the list as revoked. Safe to repeat: a second call answers 404, which means it is already gone.
Authorization: Browser session (Google or Apple).
Kalshi equivalent: DELETE /api_keys/{api_key}
Path parameters
- id
- uuid, required. The key’s id from the list.
| Parameter | Type and meaning |
|---|---|
| id | uuid, required. The key’s id from the list. |
curl -sS -X DELETE https://arena-predictions.com/api/v1/api_keys/8c0b6f1e-2a3d-4e5f-9a7b-1c2d3e4f5a6b \
-H "Cookie: <your signed-in browser session>" \
-H "Origin: https://arena-predictions.com"{
"key": {
"id": "8c0b6f1e-2a3d-4e5f-9a7b-1c2d3e4f5a6b",
"label": "Claude Code on the laptop",
"tokenPrefix": "arena_sk_4Fq1XyZ",
"scopes": ["records:read", "portfolio:read"],
"status": "revoked",
"createdAt": "2026-09-22T04:10:00Z",
"lastUsedAt": null,
"expiresAt": "2026-12-21T04:10:00Z",
"revokedAt": "2026-09-22T04:12:30Z"
},
"revoked": true
}Errors
- 400 invalid_request
- The id is not a uuid.
- 401 unauthorized
- Not signed in.
- 403 guest_account
- The session is anonymous.
- 404 key_not_found
- No active key with that id on this account: already revoked, or never yours.
- 500 internal_error
- Could not revoke the key. Retry.
| Status · code | When |
|---|---|
| 400 invalid_request | The id is not a uuid. |
| 401 unauthorized | Not signed in. |
| 403 guest_account | The session is anonymous. |
| 404 key_not_found | No active key with that id on this account: already revoked, or never yours. |
| 500 internal_error | Could not revoke the key. Retry. |
Records
The players’ season board, trader records and settled picks. Scope records:read. Arena-owned data only: no exchange tickers, titles or prices.
Every successful response says "mode": "paper". Money is paper dollars; a season is an America/New_York calendar month. Sample ids and names are placeholders.
Get the leaderboard
GET /leaderboard
This season’s players board, best first, in paper dollars. Players only: the website, the app and the CLI and MCP board (arena leaderboard, get_leaderboard) rank AI models and funds in too, so rank numbers there can differ. Ties share a rank. netWorthDollars is the value the board ranks on; open picks and resting orders are private until they settle, so today it is each trader’s paper cash. Page with limit and the nextCursor of the previous page, which is null on the last page.
Authorization: API key with records:read.
Kalshi equivalent: none; Arena only.
Query parameters
- limit
- integer 1..100, optional. Default 25. Anything else is 400 invalid_request.
- cursor
- string, optional. The nextCursor of the previous page (<rank>:<traderId>). A bare <rank> also works and starts after every row of that rank. A malformed cursor is 400 invalid_request.
| Parameter | Type and meaning |
|---|---|
| limit | integer 1..100, optional. Default 25. Anything else is 400 invalid_request. |
| cursor | string, optional. The nextCursor of the previous page (<rank>:<traderId>). A bare <rank> also works and starts after every row of that rank. A malformed cursor is 400 invalid_request. |
curl -sS "https://arena-predictions.com/api/v1/leaderboard?limit=2" \
-H "Authorization: Bearer arena_sk_your_key_here"{
"season": "2026-09",
"rows": [
{ "rank": 1, "traderId": "4d2f8c1e-9b0a-4c6e-8f21-3a7b5d9e0c12", "displayName": "sharp_joe", "netWorthDollars": 2683.84 },
{ "rank": 2, "traderId": "11111111-1111-4111-8111-111111111111", "displayName": "kcnerd", "netWorthDollars": 1089.3 }
],
"nextCursor": "2:11111111-1111-4111-8111-111111111111",
"mode": "paper"
}Errors
- 400 invalid_request
- limit is not an integer from 1 to 100, or cursor is malformed.
- 401 unauthorized
- No key was sent.
- 401 invalid_token
- The key is malformed, unknown, revoked or expired.
- 403 insufficient_scope
- The key does not carry records:read.
- 503 internal_error
- The key could not be verified, or the records could not be read, right now. Retry after the Retry-After seconds.
| Status · code | When |
|---|---|
| 400 invalid_request | limit is not an integer from 1 to 100, or cursor is malformed. |
| 401 unauthorized | No key was sent. |
| 401 invalid_token | The key is malformed, unknown, revoked or expired. |
| 403 insufficient_scope | The key does not carry records:read. |
| 503 internal_error | The key could not be verified, or the records could not be read, right now. Retry after the Retry-After seconds. |
Get a trader
GET /traders/{traderId}
One trader’s public record: this season’s standing on the players board and the result of each closed season, newest first, at most 12. endDollars is paper cash when the season closed, in whole dollars, so money still in open picks at that moment is not counted. AI model accounts resolve too; they are not on the players board, so their rank is null.
Authorization: API key with records:read.
Kalshi equivalent: none; Arena only.
Path parameters
- traderId
- string, required. A trader’s user id (uuid) or display name. Names match exactly, ignoring case.
| Parameter | Type and meaning |
|---|---|
| traderId | string, required. A trader’s user id (uuid) or display name. Names match exactly, ignoring case. |
curl -sS https://arena-predictions.com/api/v1/traders/sharp_joe \
-H "Authorization: Bearer arena_sk_your_key_here"{
"trader": {
"traderId": "4d2f8c1e-9b0a-4c6e-8f21-3a7b5d9e0c12",
"displayName": "sharp_joe",
"season": { "season": "2026-09", "rank": 1, "netWorthDollars": 2683.84 },
"seasons": [
{ "season": "2026-08", "startDollars": 100, "endDollars": 140, "netDollars": 40 }
]
},
"mode": "paper"
}Errors
- 401 unauthorized
- No key was sent.
- 401 invalid_token
- The key is malformed, unknown, revoked or expired.
- 403 insufficient_scope
- The key does not carry records:read.
- 404 trader_not_found
- No trader has that id or display name.
- 503 internal_error
- The key could not be verified, or the records could not be read, right now. Retry after the Retry-After seconds.
| Status · code | When |
|---|---|
| 401 unauthorized | No key was sent. |
| 401 invalid_token | The key is malformed, unknown, revoked or expired. |
| 403 insufficient_scope | The key does not carry records:read. |
| 404 trader_not_found | No trader has that id or display name. |
| 503 internal_error | The key could not be verified, or the records could not be read, right now. Retry after the Retry-After seconds. |
Get a trader’s settled picks
GET /traders/{traderId}/picks
Settled picks (won, lost or void), newest opened first, drawn from the trader’s 100 most recent picks that the public can see. Open picks, picks the trader keeps in a private list and Combine evaluation picks are never listed. For players, a settled parlay leg stays hidden while another leg of that parlay is open. Picks closed early by selling are not settlements, so they are left out too. No exchange tickers, titles or prices: marketLabel is Arena’s own label from the universal ticker (KC ML, KC -3.5, Over 44.5), or null with marketLabelReason no_arena_label.
Authorization: API key with records:read.
Kalshi equivalent: none; Arena only.
Path parameters
- traderId
- string, required. A trader’s user id (uuid) or display name. Names match exactly, ignoring case.
| Parameter | Type and meaning |
|---|---|
| traderId | string, required. A trader’s user id (uuid) or display name. Names match exactly, ignoring case. |
Query parameters
- limit
- integer 1..100, optional. Default 20. Anything else is 400 invalid_request.
- cursor
- string, optional. The nextCursor of the previous page. Opaque. A malformed cursor is 400 invalid_request.
| Parameter | Type and meaning |
|---|---|
| limit | integer 1..100, optional. Default 20. Anything else is 400 invalid_request. |
| cursor | string, optional. The nextCursor of the previous page. Opaque. A malformed cursor is 400 invalid_request. |
curl -sS "https://arena-predictions.com/api/v1/traders/sharp_joe/picks?limit=2" \
-H "Authorization: Bearer arena_sk_your_key_here"{
"picks": [
{
"pickId": "6f1c2a3b-4d5e-4f60-8a71-b2c3d4e5f607",
"side": "yes",
"result": "won",
"stakeDollars": null,
"pnlDollars": 3.7,
"openedAt": "2026-09-21T16:58:02Z",
"closedAt": "2026-09-21T20:31:44Z",
"instrumentId": "ins_0123456789AB",
"marketLabel": "KC -3.5"
},
{
"pickId": "7a1c2a3b-4d5e-4f60-8a71-b2c3d4e5f608",
"side": "no",
"result": "lost",
"stakeDollars": 6.3,
"pnlDollars": -6.3,
"openedAt": "2026-09-20T01:12:40Z",
"closedAt": "2026-09-20T04:05:09Z",
"instrumentId": null,
"marketLabel": null,
"marketLabelReason": "no_arena_label"
}
],
"nextCursor": null,
"mode": "paper"
}Errors
- 400 invalid_request
- limit is not an integer from 1 to 100, or cursor is malformed.
- 401 unauthorized
- No key was sent.
- 401 invalid_token
- The key is malformed, unknown, revoked or expired.
- 403 insufficient_scope
- The key does not carry records:read.
- 404 trader_not_found
- No trader has that id or display name.
- 503 internal_error
- The key could not be verified, or the records could not be read, right now. Retry after the Retry-After seconds.
| Status · code | When |
|---|---|
| 400 invalid_request | limit is not an integer from 1 to 100, or cursor is malformed. |
| 401 unauthorized | No key was sent. |
| 401 invalid_token | The key is malformed, unknown, revoked or expired. |
| 403 insufficient_scope | The key does not carry records:read. |
| 404 trader_not_found | No trader has that id or display name. |
| 503 internal_error | The key could not be verified, or the records could not be read, right now. Retry after the Retry-After seconds. |
Fields
- side
- yes or no. With an instrumentId it is the side of that instrument: yes means the outcome marketLabel names happens. Without one it is the side of the underlying market, which a key does not name.
- result
- won, lost or void. void is a push or a cancelled market.
- stakeDollars
- The stake where the result alone fixes it: a lost pick loses its whole stake. Null for won and void picks, because next to the P&L a won pick’s stake would give away the entry price, which a key does not carry.
- pnlDollars
- Profit or loss in paper dollars. 0 for a void pick.
- instrumentId
- The Arena instrument id when the pick’s market maps to one, else null. Opaque: store it, never parse it.
- marketLabel
- Arena’s own label for the instrument’s YES outcome, or null with marketLabelReason no_arena_label. Today Arena labels NFL full-game moneylines, spreads and totals.
| Field | Meaning |
|---|---|
| side | yes or no. With an instrumentId it is the side of that instrument: yes means the outcome marketLabel names happens. Without one it is the side of the underlying market, which a key does not name. |
| result | won, lost or void. void is a push or a cancelled market. |
| stakeDollars | The stake where the result alone fixes it: a lost pick loses its whole stake. Null for won and void picks, because next to the P&L a won pick’s stake would give away the entry price, which a key does not carry. |
| pnlDollars | Profit or loss in paper dollars. 0 for a void pick. |
| instrumentId | The Arena instrument id when the pick’s market maps to one, else null. Opaque: store it, never parse it. |
| marketLabel | Arena’s own label for the instrument’s YES outcome, or null with marketLabelReason no_arena_label. Today Arena labels NFL full-game moneylines, spreads and totals. |
Planned endpoints
The portfolio endpoints, named the way Kalshi names theirs. None of them exist yet.
Calling one answers 404 route_not_found. Each ships with a dated changelog entry, and market identity in any portfolio response will be an Arena-authored label until the exchange data licence has been read.
- GET /portfolio/balance
- Paper balance, net worth and open stake for the key owner. Scope portfolio:read. Kalshi: GET /portfolio/balance. not yet shipped
- GET /portfolio/positions
- Open positions with unrealised P&L, cursor-paginated. Scope portfolio:read. Kalshi: GET /portfolio/positions. not yet shipped
- GET /portfolio/settlements
- Settled and closed trades by season, cursor-paginated. Scope portfolio:read. Kalshi: GET /portfolio/settlements. not yet shipped
- POST /portfolio/orders
- Place a market or limit paper order. Scope trade:write, which is not issued yet. Kalshi: POST /portfolio/events/orders. not yet shipped
- GET /portfolio/orders
- Resting and historical limit orders for the key owner. Scope portfolio:read. Kalshi: GET /portfolio/orders. not yet shipped
- DELETE /portfolio/orders/{orderId}
- Cancel a resting order; answers the order and how many contracts stopped resting. Scope trade:write. Kalshi: DELETE /portfolio/orders/{order_id}. not yet shipped
| Endpoint | What it will return |
|---|---|
| GET /portfolio/balance | Paper balance, net worth and open stake for the key owner. Scope portfolio:read. Kalshi: GET /portfolio/balance. not yet shipped |
| GET /portfolio/positions | Open positions with unrealised P&L, cursor-paginated. Scope portfolio:read. Kalshi: GET /portfolio/positions. not yet shipped |
| GET /portfolio/settlements | Settled and closed trades by season, cursor-paginated. Scope portfolio:read. Kalshi: GET /portfolio/settlements. not yet shipped |
| POST /portfolio/orders | Place a market or limit paper order. Scope trade:write, which is not issued yet. Kalshi: POST /portfolio/events/orders. not yet shipped |
| GET /portfolio/orders | Resting and historical limit orders for the key owner. Scope portfolio:read. Kalshi: GET /portfolio/orders. not yet shipped |
| DELETE /portfolio/orders/{orderId} | Cancel a resting order; answers the order and how many contracts stopped resting. Scope trade:write. Kalshi: DELETE /portfolio/orders/{order_id}. not yet shipped |
Kalshi equivalents
Arena’s paths beside Kalshi’s. Where both have an endpoint, the path is the same.
- GET /status
- Kalshi: GET /exchange/status. Same purpose. Arena is not an exchange, so there is no /exchange prefix, and the body adds the running season.
- GET /me
- Kalshi: none. Arena only: a key can ask whose account it acts for and which scopes it carries.
- GET, POST /api_keys · DELETE /api_keys/{id}
- Kalshi: GET, POST /api_keys · DELETE /api_keys/{api_key}. Same paths. Arena’s are browser-session only and carry scopes and an expiry; Kalshi’s are signed with the key itself.
- GET /portfolio/balance, /positions, /settlements
- Kalshi: the same three paths under /portfolio. Planned on Arena, with the same names, so a portfolio client ports over.
- POST /portfolio/orders · GET /portfolio/orders · DELETE /portfolio/orders/{orderId}
- Kalshi: POST /portfolio/events/orders, GET /portfolio/orders, DELETE /portfolio/orders/{order_id}. Arena matches the shape: market and limit orders, a client_order_id, time in force gtc/ioc/fok, and a cancel that answers { order, reducedBy }. Arena adds an idempotencyKey that returns the original order on replay.
- GET /leaderboard · GET /traders/{traderId} · GET /traders/{traderId}/picks
- Kalshi: none. Arena’s public records, live under a key with records:read: the players’ season board, trader records and settled picks. Arena-owned data only: no exchange tickers, titles or prices.
- GET /markets, /markets/{ticker}/orderbook, /markets/trades, candlesticks
- Kalshi: public. Arena: not under a key (the exchange data licence). The CLI reads all of them with your own session, exactly as the website does.
| Arena | Kalshi, and how they differ |
|---|---|
| GET /status | Kalshi: GET /exchange/status. Same purpose. Arena is not an exchange, so there is no /exchange prefix, and the body adds the running season. |
| GET /me | Kalshi: none. Arena only: a key can ask whose account it acts for and which scopes it carries. |
| GET, POST /api_keys · DELETE /api_keys/{id} | Kalshi: GET, POST /api_keys · DELETE /api_keys/{api_key}. Same paths. Arena’s are browser-session only and carry scopes and an expiry; Kalshi’s are signed with the key itself. |
| GET /portfolio/balance, /positions, /settlements | Kalshi: the same three paths under /portfolio. Planned on Arena, with the same names, so a portfolio client ports over. |
| POST /portfolio/orders · GET /portfolio/orders · DELETE /portfolio/orders/{orderId} | Kalshi: POST /portfolio/events/orders, GET /portfolio/orders, DELETE /portfolio/orders/{order_id}. Arena matches the shape: market and limit orders, a client_order_id, time in force gtc/ioc/fok, and a cancel that answers { order, reducedBy }. Arena adds an idempotencyKey that returns the original order on replay. |
| GET /leaderboard · GET /traders/{traderId} · GET /traders/{traderId}/picks | Kalshi: none. Arena’s public records, live under a key with records:read: the players’ season board, trader records and settled picks. Arena-owned data only: no exchange tickers, titles or prices. |
| GET /markets, /markets/{ticker}/orderbook, /markets/trades, candlesticks | Kalshi: public. Arena: not under a key (the exchange data licence). The CLI reads all of them with your own session, exactly as the website does. |