Algorithmic Trading API

Connect a trading bot.

Endpoint Arena encourages algorithmic trading. Bring your own bot, test strategies against Season 4 markets, and use the API to quote prices, submit trades, and track results from your approved account.

How To Get Access

API access is manually approved in v1. First create a normal Endpoint Arena account and set it up in the website. Then request API access through your account page.

After an admin approves your account for API trading, open your profile and create an API key from the API Access section. The raw key is shown once, so keep it server-side and ask an admin to revoke it if it is exposed.

Your bot works with accounts, markets, quotes, trades, and statuses. Requests and responses stay at the product level.

Integration Flow

  1. Request API access from your profile and wait for admin approval.
  2. Create an API key from the API Access section in your profile.
  3. Sign in to the website, add mock funds, and enable trading for your account.
  4. Call GET /api/v1/account and check readiness.canTrade.
  5. Call GET /api/v1/markets and pick a live marketId.
  6. Call POST /api/v1/quote with the exact trade body you intend to submit.
  7. Call POST /api/v1/trades with an Idempotency-Key. The response returns as soon as the trade is accepted.
  8. Poll GET /api/v1/trades/{tradeId} until the status is settled or failed.

Authentication

Every endpoint under /api/v1 requires a bearer token.

Authorization: Bearer <endpoint_arena_api_key>
Content-Type: application/json
Idempotency-Key: <unique key>   # only for POST /api/v1/trades

Successful responses include an X-Request-Id header. Include that id when reporting integration issues.

Trade Semantics

Use the public marketId returned by the markets endpoint. Prices are probabilities from 0 to 1. Amounts are mock-USDC display values.

BUY_YES

Spend amountUsd mock USDC to buy YES shares. Limited by your available cash balance.

BUY_NO

Spend amountUsd mock USDC to buy NO shares. Limited by your available cash balance.

SELL_YES

Sell enough YES shares to target amountUsd mock USDC proceeds. Limited by live YES share holdings.

SELL_NO

Sell enough NO shares to target amountUsd mock USDC proceeds. Limited by live NO share holdings.

slippageBps is optional, defaults to 100, and can be 0 through 5000. Quote output gives the estimated shares or proceeds your bot can reason about directly.

Idempotency And Statuses

POST /api/v1/trades requires an Idempotency-Key header. Use a unique key per intended trade. Keys can be up to 200 characters.

Reusing the same key with the same normalized body returns the original trade and reused: true. Reusing the same key with a different body returns HTTP 409.

submitted

The trade was accepted and is being processed.

processing

The trade has progressed but final balances may still be catching up.

settled

The trade is final and account balances reflect it. Treat this as the final success state.

failed

The trade could not be completed. Inspect errorCode and errorMessage on the trade.

Endpoints

GET/api/v1/account

Account approval, mock-USDC cash balance, and readiness.

GET/api/v1/markets

Season 4 market summaries.

GET/api/v1/markets/{identifier}

Market detail by market id.

POST/api/v1/quote

Validate and price a proposed bot trade.

POST/api/v1/trades

Submit a trade with an Idempotency-Key.

GET/api/v1/trades/{tradeId}

Poll trade status.

Trade Request Body

{
  "marketId": "example-market",
  "action": "BUY_YES",
  "amountUsd": 10,
  "slippageBps": 100
}

For buys, amountUsd is spend. For sells, amountUsd is desired proceeds.

Response Shapes

GET /api/v1/account

{
  "account": {
    "id": "client_uuid",
    "name": "acme-bot",
    "status": "active",
    "apiTradingEnabled": true
  },
  "balances": {
    "cashUsd": 100
  },
  "readiness": {
    "canTrade": true,
    "needsAdminApproval": false,
    "needsWebSetup": false,
    "needsFunds": false,
    "needsTradingSetup": false,
    "setupUrl": "/profile",
    "message": "Ready to trade through the API."
  },
  "environment": {
    "season": "Season 4",
    "mode": "mock-USDC sandbox",
    "realMoney": false
  }
}

GET /api/v1/markets

Market objects include more trial fields than shown here. Use marketId when quoting or submitting a trade.

{
  "markets": [
    {
      "marketId": "example-market",
      "title": "Will the trial meet its primary endpoint?",
      "status": "deployed",
      "prices": { "yes": 0.57, "no": 0.43 },
      "activity": { "tradeCount": 42, "volumeUsd": 185.5, "lastTradeAt": "2026-04-26T12:00:00.000Z" },
      "trial": { "sponsorName": "Example Bio", "nctNumber": "NCT00000000" }
    }
  ]
}

POST /api/v1/quote

{
  "market": {
    "marketId": "example-market",
    "title": "Will the trial meet its primary endpoint?",
    "status": "deployed",
    "prices": { "yes": 0.57, "no": 0.43 },
    "activity": { "tradeCount": 42, "volumeUsd": 185.5, "lastTradeAt": "2026-04-26T12:00:00.000Z" },
    "trial": { "sponsorName": "Example Bio", "nctNumber": "NCT00000000" }
  },
  "balances": {
    "cashUsd": 100,
    "yesShares": 0,
    "noShares": 0
  },
  "request": {
    "marketId": "example-market",
    "action": "BUY_YES",
    "amountUsd": 1,
    "slippageBps": 100
  },
  "quote": {
    "action": "BUY_YES",
    "type": "buy",
    "side": "YES",
    "price": 0.57,
    "amountUsd": 1,
    "estimatedShares": 1.754386,
    "estimatedCostUsd": 1,
    "estimatedProceedsUsd": null,
    "slippageBps": 100,
    "minimumShares": 1.736842,
    "minimumProceedsUsd": null
  }
}

POST /api/v1/trades

New submissions return HTTP 202. Reusing the same idempotency key with the same body returns HTTP 200 and reused: true.

{
  "trade": {
    "id": "trade_uuid",
    "idempotencyKey": "bot-run-123",
    "marketId": "example-market",
    "action": "BUY_YES",
    "amountUsd": 1,
    "slippageBps": 100,
    "status": "submitted",
    "errorCode": null,
    "errorMessage": null,
    "submittedAt": "2026-04-26T12:01:00.000Z",
    "processedAt": null,
    "settledAt": null,
    "failedAt": null,
    "createdAt": "2026-04-26T12:01:00.000Z",
    "updatedAt": "2026-04-26T12:01:00.000Z"
  },
  "reused": false
}

GET /api/v1/trades/{tradeId}

Polling a trade refreshes the latest status before returning the trade object.

{
  "trade": {
    "id": "trade_uuid",
    "idempotencyKey": "bot-run-123",
    "marketId": "example-market",
    "action": "BUY_YES",
    "amountUsd": 1,
    "slippageBps": 100,
    "status": "settled",
    "errorCode": null,
    "errorMessage": null,
    "submittedAt": "2026-04-26T12:01:00.000Z",
    "processedAt": "2026-04-26T12:01:08.000Z",
    "settledAt": "2026-04-26T12:01:10.000Z",
    "failedAt": null,
    "createdAt": "2026-04-26T12:01:00.000Z",
    "updatedAt": "2026-04-26T12:01:10.000Z"
  }
}

GET /api/v1/trades

Returns the 50 most recent trades for the authenticated API client. Each item has the same full trade shape as the single-trade response.

{
  "trades": [
    {
      "id": "trade_uuid",
      "marketId": "example-market",
      "action": "BUY_YES",
      "amountUsd": 1,
      "status": "settled",
      "createdAt": "2026-04-26T12:01:00.000Z",
      "updatedAt": "2026-04-26T12:01:10.000Z"
    }
  ]
}

Errors

Errors use the same JSON shape and also include X-Request-Id.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "action must be one of: BUY_YES, BUY_NO, SELL_YES, SELL_NO",
    "requestId": "request_uuid",
    "details": {}
  }
}

Toy TS Example

Paste this into a local TypeScript file, set an API key and market id, then run a 1 mock USDC BUY_YES.

// Toy TypeScript starter.
// Run with:
// ENDPOINT_ARENA_API_KEY=... MARKET_ID=... npx tsx toy-endpointarena.ts

const baseUrl = (process.env.ENDPOINT_ARENA_API_BASE_URL || 'https://endpointarena.com').replace(/\/$/, '')
const apiKey = process.env.ENDPOINT_ARENA_API_KEY || ''
const marketId = process.env.MARKET_ID || ''
const tradeBody = {
  marketId,
  action: 'BUY_YES',
  amountUsd: 1,
  slippageBps: 100,
}

async function endpoint<T>(path: string, init: RequestInit = {}): Promise<T> {
  const headers = new Headers(init.headers)
  headers.set('Authorization', 'Bearer ' + apiKey)
  if (init.body && !headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json')
  }

  const response = await fetch(baseUrl + path, { ...init, headers })
  const payload = await response.json().catch(() => null)
  if (!response.ok) {
    throw new Error(payload?.error?.message || 'Request failed with ' + response.status)
  }
  return payload as T
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

async function main() {
  if (!apiKey) throw new Error('ENDPOINT_ARENA_API_KEY is required')
  if (!marketId) throw new Error('MARKET_ID is required')

  const account = await endpoint<{
    balances: { cashUsd: number }
    readiness: { canTrade: boolean; needsWebSetup: boolean; needsAdminApproval: boolean }
  }>('/api/v1/account')
  console.log('cash balance', account.balances.cashUsd)

  if (account.readiness.needsAdminApproval) {
    throw new Error('This account is not approved for API trading yet')
  }
  if (account.readiness.needsWebSetup) {
    throw new Error('Open Endpoint Arena in the browser to add mock funds and enable trading first')
  }

  const market = await endpoint<{ market: { marketId: string; title: string } }>(
    '/api/v1/markets/' + encodeURIComponent(marketId),
  )
  console.log('market', market.market.marketId, market.market.title)

  const quote = await endpoint<{ quote: { price: number; estimatedShares: number } }>('/api/v1/quote', {
    method: 'POST',
    body: JSON.stringify(tradeBody),
  })
  console.log('quote', quote.quote)

  const submitted = await endpoint<{ trade: { id: string; status: string } }>('/api/v1/trades', {
    method: 'POST',
    headers: { 'Idempotency-Key': 'toy-ts-' + Date.now() },
    body: JSON.stringify(tradeBody),
  })
  console.log('submitted', submitted.trade)

  for (let attempt = 0; attempt < 20; attempt += 1) {
    const status = await endpoint<{ trade: { status: string; errorMessage: string | null } }>(
      '/api/v1/trades/' + encodeURIComponent(submitted.trade.id),
    )
    console.log('status', status.trade.status)
    if (status.trade.status === 'settled') return
    if (status.trade.status === 'failed') throw new Error(status.trade.errorMessage || 'Trade failed')
    await sleep(3_000)
  }
}

void main().catch((error) => {
  console.error(error instanceof Error ? error.message : error)
  process.exitCode = 1
})