Portfolio
All endpoints in this section require authentication. See Authentication.
Rate limit: v1-auth: 120 req / 60 s per IP
GET /api/v1/me
Returns your account information and balance.
Auth: Required
Response
{
"id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"displayName": "Alice",
"email": "[email protected]",
"balance": 987600,
"createdAt": "2025-06-01T12:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
id | UUID string | Your user id |
displayName | string | Display name |
email | string | Account email |
balance | integer | EUR-cent balance |
createdAt | ISO 8601 | Account creation time |
GET /api/v1/me/orders
Returns your paginated order history.
Auth: Required
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
status | "open" | "filled" | "all" | "open" | "open" returns open + partially_filled; "filled" returns fully filled only; "all" returns all |
page | integer | 1 | Page number |
limit | integer | 20 | Results per page (1–50) |
Unlike GET /orders, this endpoint does not accept "cancelled" as a status filter. Use GET /orders?status=cancelled if you need cancelled orders.
Response
{
"data": [
{
"id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"userId": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"marketId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"marketTitle": "Will Reform Party win the most seats?",
"side": "yes",
"price": 55,
"quantity": 100,
"filledQuantity": 40,
"status": "partially_filled",
"orderType": "buy",
"createdAt": "2026-01-10T09:00:00.000Z",
"expiresAt": null
}
],
"meta": {
"total": 87,
"page": 1,
"limit": 20,
"hasMore": true
}
}GET /api/v1/me/portfolio
Returns your balance, all open positions (quantity > 0), and all currently resting orders in a single call. Designed for portfolio page polling.
Auth: Required
Response
{
"balance": 987600,
"positions": [
{
"id": "f6a7b8c9-d0e1-2345-f012-456789012345",
"marketId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"marketTitle": "Will Reform Party win the most seats?",
"marketStatus": "open",
"lastTradePrice": 58,
"side": "yes",
"quantity": 60,
"avgPrice": 54,
"realisedPnl": 0
}
],
"openOrders": [
{
"id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"marketId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"marketTitle": "Will Reform Party win the most seats?",
"side": "yes",
"price": 55,
"quantity": 100,
"filledQuantity": 40,
"status": "partially_filled",
"orderType": "buy",
"createdAt": "2026-01-10T09:00:00.000Z"
}
]
}Portfolio fields
| Field | Type | Description |
|---|---|---|
balance | integer | EUR-cent balance |
positions | array | All positions with quantity > 0, ordered by market close time |
openOrders | array | Active orders (status scheduled, open, or partially_filled), newest first; capped at 200. Scheduled rows include releaseAt and budgetCents |
Position fields
| Field | Type | Description |
|---|---|---|
id | UUID string | Position id |
marketId | UUID string | Market |
marketTitle | string | Market title |
marketStatus | string | Current market status |
lastTradePrice | integer | null | Last traded price (1–99) |
side | "yes" | "no" | Which side you hold |
quantity | integer | Shares held |
avgPrice | integer | Weighted average cost (1–99) |
realisedPnl | integer | EUR-cents realised after resolution |
GET /api/v1/user/stream
Server-Sent Events stream for live-order lifecycle and matching-side-effect notifications.
Auth: Required
Rate limit: sse:user: 30 / 60 s per user (120 / 60 s per IP)
Server-side and API-key clients connect to the v1 path with their jk_ key as Authorization: Bearer. From the browser, use the same-origin proxy at /api/stream/user, which authenticates with the cookie session: EventSource can’t send an Authorization header.
Auth and rate-limit errors are returned as real HTTP statuses before the stream opens, as JSON, not as SSE frames. A missing or invalid key returns 401; exceeding either sse:user tier (30 / 60 s per user or 120 / 60 s per IP) returns 429 with a Retry-After header.
Connection frame
On connect the server immediately sends:
{ "type": "connected" }Event types
All subsequent frames have a type field:
order_pending
Sent once when a new live-market order is accepted into the delay window. Idempotent POST retries do not emit it again.
{
"type": "order_pending",
"orderId": "d4e5f6a7-b8c9-0123-def0-234567890123",
"marketId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"side": "yes",
"price": 55,
"quantity": 100,
"releaseAt": "2026-01-15T14:32:03.000Z",
"budgetCents": null
}order_released
Sent once after the delayed order is revalidated and matched against the current book. Inspect status to determine the outcome; a release-time validation failure is represented by status: "cancelled" on this event, not by order_cancelled.
{
"type": "order_released",
"orderId": "d4e5f6a7-b8c9-0123-def0-234567890123",
"marketId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"status": "partially_filled",
"side": "yes",
"price": 55,
"quantity": 100,
"filledQuantity": 40,
"cancelReason": null,
"fills": 2,
"balance": 976400,
"feeCents": 18
}status is open | partially_filled | filled | cancelled. fills is the number of trades generated during release, not an array. balance and feeCents may be null when no value was produced.
maker_fill
Published after one or more of your resting maker orders fill. One completed match emits at most one event to each distinct maker, aggregating that maker’s unique resting order IDs. It is an invalidation signal: fetch authoritative balance, position, and order state after receiving it.
{
"type": "maker_fill",
"marketId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"orderIds": [
"d4e5f6a7-b8c9-0123-def0-234567890123",
"e5f6a7b8-c9d0-1234-ef01-345678901234"
],
"fillCount": 3
}The event deliberately carries no balance. The incoming match result’s balance belongs to the taker, and one maker event can cover several resting orders. Redis Pub/Sub does not replay events missed during a reconnect gap, so clients should retain a slow reconciliation poll or re-fetch on reconnect rather than relying on pure push.
order_cancelled
Sent when matching cancels one of your resting maker orders as a side effect, for example because it is underfunded or the position is no longer sufficient. Direct DELETE responses are authoritative for user-initiated cancellation.
{
"type": "order_cancelled",
"orderId": "d4e5f6a7-b8c9-0123-def0-234567890123",
"marketId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"marketTitle": "Will Reform Party win the most seats?",
"side": "yes",
"price": 55,
"quantity": 100,
"filledQuantity": 40,
"cancelReason": "market_closed"
}Possible cancelReason values across cancellation and release events: user_cancelled | user_amended | insufficient_funds | market_closed | position_insufficient | expired | trading_paused | self_trade | post_only_would_cross | insufficient_liquidity | self_excluded | loss_limit
Keep-alive
The server writes : ping comments every 25 seconds. Reconnect on any network error.
Errors
| Status | Body | When |
|---|---|---|
401 | { "error": "Unauthorized" } | Missing or invalid token |
429 | { "error": "Too many requests" } | sse:user limit exceeded: per user (30 / 60 s) or per IP (120 / 60 s) |