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#
Most endpoints are served under the versioned prefix:
/api/v1/<endpoint> # recommended
/api/<endpoint> # legacy, deprecated
Requests to the legacy unversioned /api/ prefix still work but receive
Deprecation: true, Sunset, and Link: ...; rel="successor-version" response
headers. Migrate to /api/v1/.
A small set of endpoints is additionally exposed at the host root (no /api
prefix): the /v2/... read endpoints (fee config, referrals, wallet analytics),
/config, /trades, /orderbook/top, and the ops endpoints /health, /ready,
and /metrics. Endpoint pages in this section write paths without a prefix; unless
marked root-level, prepend /api/v1.
Response Format#
Success response#
Standard endpoints wrap their payload in a data envelope:
{
"data": { ... }
}
A few diagnostic endpoints (/config, /orderbook/top, /trades, /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 | Auth only, optional — solana (default) or tempo |
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 |
Deprecation/Sunset | Sent on legacy unversioned /api/ requests |
Rate Limits#
All windows default to 60 seconds (RATE_LIMIT_WINDOW_MS).
| Tier | Requests/window | Notes |
|---|---|---|
| Public (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) | Stricter per-window limit (RATE_LIMIT_SENSITIVE_MAX) |
| Compliance | 5 (default) | RATE_LIMIT_COMPLIANCE_MAX |
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; solana (default) or tempo |
Auth message format:
The x-wallet-message value is a JSON string containing these fields:
{
"domain": "api.seesaw.markets",
"timestamp": 1706745600,
"action": "AUTH",
"nonce": "unique-random-string"
}
domainis the server's hostname (e.g.api.seesaw.markets).timestampmust be within 5 minutes of server time and no more than 30 seconds in the future.actionis"AUTH"for REST authentication. 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 (default) or tempo |
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",
"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 feeConfig field carries the fee-curve parameters used by the slippage
estimator in @seesaw/core. feeCapBps and decayRateBps default to 200 and
600 when no prior UpdateFeeConfig 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 /orders
Without the ?tx parameter, this endpoint requires authentication and returns
the authenticated wallet's orders. With ?tx=<sig>, it 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 a 403 is returned.
Stats#
All stats endpoints require authentication.
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) |
offset | integer | No | Pagination offset (default: 0) |
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
}
}
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 |
| 401 | UNAUTHORIZED | Missing or invalid authentication |
| 403 | UNAUTHORIZED | Attempting to access another user's data |
| 404 | NOT_FOUND | Resource not found |
| 409 | CONFLICT | Conflicting operation |
| 429 | RATE_LIMITED | Rate limit exceeded |
| 451 | GEO_BLOCKED | Request from restricted jurisdiction |
| 500 | INTERNAL_ERROR | Server error (details sanitized) |
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