REST API — Conventions and Authenticated Endpoints#
API conventions (base URLs, path prefixes, response envelope, headers, rate limits, data types, pagination) and authenticated endpoints for the Seesaw indexer.
- Base URL (production):
https://api.seesaw.markets
A machine-readable OpenAPI 3.0 specification is available alongside these docs at
api-reference/openapi.yamlfor generating clients and tooling.
Contents#
- Path Prefixes and Versioning
- Response Format
- Common Headers
- Rate Limits
- Data Types
- Pagination
- Authentication
- Markets
- Orders
- Positions
- Stats
- Achievements
- Error Codes
For the public market, orderbook, position, trade, referral, and creator read endpoints, see Endpoints.
Path Prefixes and Versioning#
The canonical REST base path is:
/api/v1/<endpoint>
The /v2/... paths are fee/referral feature paths inside API v1, so their
canonical form is /api/v1/v2/... (for example,
GET /api/v1/v2/fee-config). Older root-level aliases remain served for
compatibility: /v2/..., /tx/..., /orders?tx=..., /config, /trades,
/orderbook/top, /status, and /admin/.... Those aliases return
Deprecation: true, Link: </api/v1/...>; rel="canonical", and
X-Seesaw-Canonical-Path headers. New integrations should use /api/v1
forms only.
The ops endpoints /health, /ready, and /metrics remain root-level and are
not part of the versioned REST API.
Endpoint pages in this section write paths without a prefix; prepend /api/v1
unless a row explicitly says it is an operational root endpoint.
The web application also owns private same-origin Next.js app routes:
/api/rpc for browser RPC proxying, /api/pyth for the Pyth stream proxy, and
/api/health for web health checks. These are not indexer REST API aliases and
should be referenced through deployment configuration or client helpers.
Response Format#
Success response#
Standard endpoints wrap their payload in a data envelope:
{
"data": { ... }
}
A few diagnostic endpoints (/config, /orderbook/top, /trades, /orders?tx=..., /health)
return their payload unwrapped (no data envelope). Each endpoint page shows
the exact shape.
Error response#
{
"error": "Market not found",
"code": "NOT_FOUND"
}
Some validation errors include an optional details field. Error responses never
expose internal state, stack traces, or database details.
Common Headers#
Request headers#
| Header | Description |
|---|---|
Content-Type | application/json for POST/PUT bodies |
x-request-id | Optional client-supplied correlation ID (echoed back if provided) |
x-wallet-signature | Auth only — base58 Ed25519 signature (see Authentication) |
x-wallet-message | Auth only — the signed JSON auth message |
x-wallet-pubkey | Auth only — base58 wallet public key |
x-wallet-chain | Optional compatibility header; if supplied, it must match signed message chain |
Response headers#
| Header | Description |
|---|---|
x-request-id | Request correlation ID |
X-RateLimit-Limit | Rate limit ceiling for your tier |
X-RateLimit-Remaining | Remaining requests in the current window |
X-RateLimit-Reset | Unix time (seconds) when the window resets |
Retry-After | Seconds to wait, sent with 429 responses |
Rate Limits#
All windows default to 60 seconds (RATE_LIMIT_WINDOW_MS).
| Tier | Requests/window | Notes |
|---|---|---|
| Public market reads | 300 (default) | GET reads keyed per IP; PUBLIC_READ_RATE_LIMIT_MAX_REQUESTS |
| Public other no auth | 100 (default) | Configurable via RATE_LIMIT_MAX_REQUESTS |
| Authenticated | 2× public | Keyed per authenticated wallet |
| Write operations | ½ public | POST / PUT / PATCH / DELETE |
| Sensitive ops | 20 (default) | /orders, /positions, /tx/*, /admin/*, configured by RATE_LIMIT_SENSITIVE_MAX |
| Compliance | 5 (default) | RATE_LIMIT_COMPLIANCE_MAX |
Public market reads cover markets, market detail, order book, trades/fills,
resolution fields on market detail, /status, /config, and /stats/platform.
Use PUBLIC_READ_RATE_LIMIT_WINDOW_MS to set a separate read window; when it is
unset, it follows RATE_LIMIT_WINDOW_MS.
WebSocket connections: default max 10 per IP (WS_MAX_CONNECTIONS_PER_IP), 1000
total (WS_MAX_CLIENTS). These are deployment-configurable, not per-tier.
Requests that exceed a limit receive 429 with a Retry-After header. Handlers
that run longer than 30 seconds return 504 with code TIMEOUT.
Data Types#
All token amounts and share quantities are strings of integer base units
(settlement-mint base units, e.g. 6 decimals for USDT). Prices are integer basis
points in [0, 10000] on the canonical YES book.
Market (list item)#
interface MarketListItem {
id: string;
address: string; // market PDA (base58)
marketId: string; // epoch id = floor(unix_time / duration)
pythFeed: string | null; // push-oracle feed address; pull-mode markets store no address
pythFeedId: string | null; // 32-byte Pyth feed id (hex, no 0x prefix) — PDA seed
state: number; // 0=Pending, 1=Created, 2=Trading, 3=Settling, 4=Resolved, 5=Closed
outcome: number | null;
tStart: string; // ISO timestamp
tEnd: string;
startPrice: string | null;
startPriceConf: string | null;
startPriceExpo: number | null;
endPrice: string | null;
endPriceConf: string | null;
endPriceExpo: number | null;
creator: string | null; // PDA seed; needed for lifecycle cranks
durationSeconds: string | null; // u64 as string; PDA seed
maxConfidenceRatioBps: number | null;
totalVolume: string;
totalTrades: number;
totalPositions: number;
bestBid: number | null; // bps
bestAsk: number | null; // bps
}
The market detail endpoint adds share totals, mints, settlement mint,
accumulatedCreatorFees, and the live fee-curve parameters
feeConfig: { feeCapBps, decayRateBps } — see Endpoints.
Orderbook level#
interface OrderbookLevel {
price: number; // bps
quantity: string; // base units, aggregated at this level
orders: number; // resting order count at this level
}
Position#
interface Position {
id: number;
address: string;
marketAddress: string;
owner: string;
market: { pythFeed: string | null; state: number; outcome: number | null };
yesShares: string;
noShares: string;
collateralDeposited: string;
totalBought: string;
totalSold: string;
settled: boolean;
payout: string;
currentYesPrice: number; // bps; mid of best bid/ask, with fallbacks
}
Order#
interface Order {
id: number;
orderId: string;
marketAddress: string;
marketId: string;
settlementMint: string;
owner: string;
side: 'yes' | 'no';
sideCode: number; // 0=buy_yes, 1=sell_yes, 2=buy_no, 3=sell_no
sideLabel: 'buy_yes' | 'sell_yes' | 'buy_no' | 'sell_no';
isYes: boolean;
priceBps: number;
quantity: string;
filledQuantity: string;
status: 'open' | 'filled' | 'cancelled' | 'expired' | 'pending_cancel' | 'cancel_failed';
statusCode: number;
orderType: number;
createdAt: string | null;
updatedAt: string | null;
}
Trade#
Trades are returned with snake_case fields (market_address, price_bps,
taker_fee, protocol_treasury_index, …) — see
Endpoints → Trades.
Pagination#
List endpoints use offset-based pagination:
GET /api/v1/markets?limit=20&offset=0
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Items per page (max 100) |
offset | number | 0 | Number of items to skip |
Responses include pagination info inside the data envelope:
{
"data": {
"markets": [...],
"total": 250,
"limit": 50,
"offset": 0
}
}
Authentication#
Authenticated endpoints require a Solana wallet signature. The server verifies an Ed25519 signature over a structured auth message on every authenticated request.
Required headers for authenticated routes:
| Header | Description |
|---|---|
x-wallet-pubkey | Base58-encoded Solana wallet public key |
x-wallet-signature | Base58-encoded Ed25519 signature of the auth message |
x-wallet-message | The signed auth message (JSON string, see below) |
x-wallet-chain | Optional compatibility header; must match signed chain if present |
Auth message format:
The x-wallet-message value is a JSON string containing these fields:
{
"domain": "api.seesaw.markets",
"chain": "solana",
"timestamp": 1706745600,
"action": "GET /positions",
"nonce": "unique-random-string"
}
domainis the server's hostname (e.g.api.seesaw.markets).chainis the signed wallet-chain binding. Solana beta clients sign"solana". Legacy messages without this field are treated as Solana-only, but a suppliedx-wallet-chainheader must match the signed value.timestampmust be within 5 minutes of server time and no more than 30 seconds in the future.actionis the exact authenticated REST request target asMETHOD /path, including the query string when present, after stripping the/api/v1prefix for versioned routes. Examples:GET /positions,GET /positions?owner=<wallet>,POST /tx/place-order. WebSocket subscriptions use"WS_SUBSCRIBE <channel>"— see WebSockets.noncemust be unique across HTTP and WebSocket auth — the server rejects replayed nonces.- The signature is an Ed25519 signature over the exact
x-wallet-messagestring bytes, base58-encoded.
Public routes (no authentication required):
GET /markets,GET /markets/current,GET /markets/:marketIdGET /orders?tx=<sig>(transaction log lookup; returns trades by signature)GET /leaderboard,GET /achievements/all,GET /challenges/todayGET /v2/fee-config,GET /v2/referral/:address,GET /v2/referrer/:address/earningsGET /v2/referrals/leaderboard,GET /v2/wallet/:address/analyticsGET /orderbook/top,GET /trades,GET /status
Markets#
List Markets#
GET /markets
Authentication: None required
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
state | integer | No | Filter by market state (0=Pending, 1=Created, 2=Trading, 3=Settling, 4=Resolved, 5=Closed) |
limit | integer | No | Items per page (default: 50, max: 100) |
offset | integer | No | Pagination offset (default: 0, max: 1,000,000) |
chain | string | No | Solana beta accepts only solana; omit to use Solana |
Response:
{
"data": {
"markets": [
{
"id": 1,
"address": "ABcd...1234",
"marketId": "1937600",
"pythFeed": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace",
"state": 2,
"outcome": null,
"tStart": "2026-02-12T00:00:00.000Z",
"tEnd": "2026-02-12T00:15:00.000Z",
"startPrice": "68523400000",
"startPriceExpo": -8,
"totalVolume": "15000000",
"totalTrades": 42,
"totalPositions": 18
}
],
"total": 96,
"limit": 50,
"offset": 0
}
}
Get Current Market#
GET /markets/current
Authentication: None required
Response:
{
"data": {
"market": {
"id": 1,
"address": "ABcd...1234",
"marketId": "1937600",
"pythFeed": "0xff61...",
"state": 2,
"outcome": null,
"tStart": "2026-02-12T00:00:00.000Z",
"tEnd": "2026-02-12T00:15:00.000Z",
"startPrice": "68523400000",
"startPriceConf": "34000000",
"startPriceExpo": -8,
"endPrice": null,
"endPriceConf": null,
"endPriceExpo": null,
"totalYesShares": "5000000",
"totalNoShares": "3000000",
"totalVolume": "15000000",
"totalTrades": 42,
"totalPositions": 18,
"createdAt": "2026-02-12T00:00:01.000Z",
"resolvedAt": null
},
"status": "trading"
}
}
The status field is either "trading" (current market) or "upcoming" (next
market if no current one). Returns 404 if no current or upcoming market exists.
Note: totalCollateral is a deprecated field frozen at the create-time value
since protocol upgrade F-020. Do not rely on it.
Get Market by ID#
GET /markets/:marketId
Authentication: None required
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
marketId | string | Market address (base58) or numeric market ID |
Response:
{
"data": {
"market": {
"...": "all market list fields",
"accumulatedCreatorFees": "12000",
"tickSizeBps": 100,
"feeConfig": {
"feeCapBps": 200,
"decayRateBps": 600
}
},
"orderbook": {
"bids": [
{ "price": 6500, "quantity": "10000000", "orders": 3 },
{ "price": 6000, "quantity": "5000000", "orders": 1 }
],
"asks": [
{ "price": 7000, "quantity": "8000000", "orders": 2 },
{ "price": 7500, "quantity": "3000000", "orders": 1 }
]
}
}
}
The tickSizeBps field carries the live order-book tick size used for
canonical YES-price rounding previews. feeConfig carries the fee-curve
parameters used by the slippage estimator in @seesaw/core. tickSizeBps
defaults to 100 and feeCapBps/decayRateBps default to 200/600 when no prior
config event has been indexed. The orderbook shows the top 10 price levels on
each side. Prices are in basis points (0–10000). Bids are sorted descending,
asks ascending.
Orders#
List User Orders (Authenticated)#
GET /api/v1/orders
Without the ?tx parameter, this endpoint requires authentication and returns
the authenticated wallet's orders. The root-level GET /orders?tx=<sig> lookup
is public and returns the trades logged in that transaction.
Authentication: Required (omit for ?tx lookups)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tx | string | No | On-chain transaction signature (public) |
status | string | No | Filter: open, filled, or cancelled |
limit | integer | No | Items per page (default: 50, max: 100) |
offset | integer | No | Pagination offset (default: 0) |
Response:
{
"data": {
"orders": [
{
"id": 42,
"orderId": "7",
"marketAddress": "ABcd...1234",
"marketId": "1937600",
"settlementMint": "EPjF...Dt1v",
"owner": "Wallet...Address",
"side": "yes",
"sideCode": 0,
"sideLabel": "buy_yes",
"isYes": true,
"priceBps": 6500,
"quantity": "1000000",
"filledQuantity": "500000",
"status": "open",
"statusCode": 0,
"orderType": 0,
"createdAt": "2026-02-12T00:05:00.000Z",
"updatedAt": "2026-02-12T00:06:00.000Z"
}
],
"total": 15,
"limit": 50,
"offset": 0
}
}
Order status values: open, filled, cancelled, expired,
pending_cancel, cancel_failed
Note: pending_cancel is an optimistic state set when a cancellation is
submitted by the app. The indexer reconciles this when the on-chain CancelOrder
event is observed. Order cancellation is always submitted on-chain via the SDK or
CLI; there is no REST-only cancel operation.
Positions#
List User Positions#
GET /positions
Authentication: Required
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
settled | string | No | Filter: true or false |
owner | string | No | Must match authenticated wallet (for compatibility) |
limit | integer | No | Items per page (default: 50, max: 100) |
offset | integer | No | Pagination offset (default: 0) |
Response:
{
"data": {
"positions": [
{
"id": 7,
"address": "Pos...Address",
"marketAddress": "ABcd...1234",
"owner": "Wallet...Address",
"market": {
"pythFeed": "0xff61...",
"state": 4,
"outcome": 1
},
"yesShares": "1000000",
"noShares": "0",
"collateralDeposited": "650000",
"totalBought": "1000000",
"totalSold": "0",
"settled": false,
"payout": "0",
"currentYesPrice": 6500
}
],
"balance": "5000000",
"total": 3,
"limit": 50,
"offset": 0
}
}
The balance field is the sum of redeemable payouts from settled positions.
currentYesPrice is the mid of best bid/ask in basis points, with fallbacks.
If the owner query parameter is provided, it must exactly match the
authenticated wallet address; otherwise the server returns 404 for
anti-enumeration. Missing or invalid wallet authentication still returns 401.
Stats#
Wallet summary/history/trade stats require authentication. The public wallet
P&L-history route is GET /stats/user/:wallet/pnl-history.
Get Personal Stats#
GET /stats
Authentication: Required
Response:
{
"data": {
"totalTrades": 142,
"winningTrades": 85,
"losingTrades": 57,
"totalPnl": "12500000",
"winRate": 59.86,
"avgTradeSize": "500000",
"bestTradePnl": "2000000",
"worstTradePnl": "-1500000",
"currentStreak": 3,
"longestStreak": 7
}
}
If the user has no stored stats record, stats are computed on-the-fly from positions and trades (capped at 10,000 records).
Get P&L History#
GET /stats/history
Authentication: Required
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
days | integer | No | Number of days of history |
Response:
{
"data": {
"history": [
{ "date": "2026-02-10T00:00:00.000Z", "value": -2.5 },
{ "date": "2026-02-11T00:00:00.000Z", "value": 1.3 },
{ "date": "2026-02-12T00:00:00.000Z", "value": 5.7 }
]
}
}
Values are cumulative P&L in USDT (converted from base units by dividing by 1,000,000). Daily P&L is summed from settled positions.
Get Trade History#
GET /stats/trades
Authentication: Required
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Items per page (default: 20, max: 1000). The server fetches limit + 1. |
cursor | string | No | Opaque keyset cursor returned as nextCursor. Cannot be combined with offset. |
offset | integer | No | Legacy pagination offset (default: 0). Cannot be combined with cursor. |
outcome | string | No | Filter: win or loss |
Response:
{
"data": {
"trades": [
{
"id": 42,
"marketId": "1937600",
"side": "yes",
"shares": "1000000",
"cost": "650000",
"payout": "1000000",
"pnl": "350000",
"outcome": "win",
"marketOutcome": "up",
"settledAt": "2026-02-12T00:16:00.000Z",
"tStart": "2026-02-12T00:00:00.000Z",
"tEnd": "2026-02-12T00:15:00.000Z"
}
],
"total": 142,
"hasMore": true,
"nextCursor": "eyJleGVjdXRlZF9hdCI6IjIwMjYtMDItMTJUMDA6MDU6MDAuMDAwWiJ9"
}
}
Get Wallet P&L History#
GET /stats/user/:wallet/pnl-history
Authentication: Wallet signature required
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
days | integer | No | Number of days of history |
Response: Same history envelope shape as GET /stats/history, scoped to
the wallet in the path.
Achievements#
List All Achievements (Public)#
GET /achievements/all
Authentication: None required
Response:
{
"data": {
"achievements": [
{
"id": 1,
"code": "first_trade",
"name": "First Steps",
"description": "Complete your first trade",
"category": "trading",
"iconUrl": null,
"threshold": 1,
"createdAt": "2026-02-01T00:00:00.000Z"
}
]
}
}
Achievement categories: trading, streak, accuracy, exploration
Get User Achievements#
GET /achievements
Authentication: Required
Response:
{
"data": {
"achievements": [
{
"id": 1,
"code": "first_trade",
"name": "First Steps",
"description": "Complete your first trade",
"category": "trading",
"iconUrl": null,
"threshold": 1,
"createdAt": "2026-02-01T00:00:00.000Z",
"progress": 1,
"unlockedAt": "2026-02-05T12:00:00.000Z"
}
],
"grouped": {
"trading": [],
"streak": [],
"accuracy": [],
"exploration": []
},
"stats": {
"total": 13,
"unlocked": 5
}
}
}
Error Codes#
| HTTP Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request parameters or body |
| 400 | INVALID_REQUEST_BODY | Request body could not be parsed |
| 400 | INVALID_PARAMETER | Invalid path or query parameter |
| 400 | MISSING_REQUIRED_FIELD | Required request field is absent |
| 400 | LIMIT_EXCEEDED | Per-resource cap exceeded |
| 401 | UNAUTHORIZED | Missing or invalid authentication |
| 401 | MISSING_AUTH_HEADERS | Required wallet-auth headers are missing |
| 401 | INVALID_SIGNATURE | Wallet signature verification failed |
| 401 | MESSAGE_EXPIRED | Signed auth message is stale or future |
| 404 | NOT_FOUND | Resource not found |
| 404 | USER_NOT_FOUND | User profile not found |
| 404 | RESOURCE_NOT_FOUND | Requested resource not found |
| 405 | METHOD_NOT_ALLOWED | HTTP method is not supported |
| 409 | CONFLICT | Conflicting operation |
| 429 | RATE_LIMITED | Rate limit exceeded |
| 451 | GEO_BLOCKED | Request from restricted jurisdiction |
| 451 | ATTESTATION_REQUIRED | Jurisdiction attestation required |
| 500 | INTERNAL_ERROR | Server error (details sanitized) |
| 500 | DATABASE_ERROR | Database operation failed |
| 503 | SERVICE_UNAVAILABLE | Required service unavailable |
| 504 | TIMEOUT | Request exceeded the route timeout |
Error responses never expose internal state, stack traces, or database details.
See Also#
- API Reference overview — section map and quick start
- Endpoints — public market, orderbook, position, trade, referral, and creator reads
- WebSockets — real-time subscriptions