> ## Documentation Index
> Fetch the complete documentation index at: https://docs.seer.pm/llms.txt
> Use this file to discover all available pages before exploring further.

# Netlify Functions – Reference

Documentation for the serverless functions exposed by the app. Useful for integrations, frontend, and AI tools working with this codebase.

**Base URL:** `https://app.seer.pm`

Functions are available at: `https://app.seer.pm/.netlify/functions/<function-name>`

***

## get-market

Fetches a single market by ID or URL/slug. Merges real-time subgraph data with cached database data (including verification status) for maximum accuracy.

| Field | Detail |
| - | - |
| **Method** | `POST` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-market` |
| **Content-Type** | `application/json` |

### Body (JSON)

| Parameter | Type | Required | Description |
| - | - | - | - |
| `chainId` | number | Yes | Network ID (SupportedChain). |
| `id` | string (Address) | Yes\* | Market ID (0x…). \*Required if `url` is not provided. |
| `url` | string | Yes\* | Market slug/URL (e.g. human-readable name). \*Required if `id` is not provided. |

### Response 200

Serialized market object: all `bigint` fields are returned as `string` for JSON compatibility.

```ts theme={null}
type SerializedMarket = {
  chainId: number;
  factory?: `0x${string}`;
  outcomesSupply: string;
  liquidityUSD: number;
  incentive: number;
  hasLiquidity: boolean;
  categories: string[];
  poolBalance: ({
    token0: { symbol: string; balance: number };
    token1: { symbol: string; balance: number };
  } | null)[];
  odds: (number | null)[];
  creator?: string | null;
  blockTimestamp?: number;
  verification?: { status: "verified" | "verifying" | "challenged" | "not_verified"; itemID?: string; deadline?: number };
  images?: { market: string; outcomes: string[] };
  index?: number;
  url: string;
  id: `0x${string}`;
  type: "Generic" | "Futarchy";
  marketName: string;
  outcomes: readonly string[];
  collateralToken: `0x${string}`;
  collateralToken1: `0x${string}`;
  collateralToken2: `0x${string}`;
  wrappedTokens: `0x${string}`[];
  parentMarket: {
    id: `0x${string}`;
    conditionId: `0x${string}`;
    payoutReported: boolean;
    payoutNumerators: readonly string[];
  };
  parentOutcome: string;
  parentCollectionId: `0x${string}`;
  conditionId: `0x${string}`;
  questionId: `0x${string}`;
  templateId: string;
  questions: Array<{
    id: `0x${string}`;
    arbitrator: `0x${string}`;
    opening_ts: number;
    timeout: number;
    finalize_ts: number;
    is_pending_arbitration: boolean;
    best_answer: `0x${string}`;
    bond: string;
    min_bond: string;
    base_question: `0x${string}`;
  }>;
  openingTs: number;
  finalizeTs: number;
  encodedQuestions: readonly string[];
  lowerBound: string;
  upperBound: string;
  payoutReported: boolean;
  payoutNumerators: readonly string[];
};
```

### Errors

* **400** – `Missing request body` or `Missing required parameters: chainId and (id or url)`.
* **404** – `Market not found`.
* **500** – Internal server error.

***

## markets-search

Search and list markets with filters, pagination, and sorting. Queries one or multiple chains; when `VITE_TESTNET_WEBSITE=1` only Sepolia is used.

| Field | Detail |
| - | - |
| **Method** | `POST` |
| **URL** | `https://app.seer.pm/.netlify/functions/markets-search` |
| **Content-Type** | `application/json` |

### Body (JSON) – Search parameters

All optional. Defaults in the handler: `limit=1000`, `page=1`, chains = all supported (or Sepolia only on testnet).

| Parameter | Type | Description |
| - | - | - |
| `chainsList` | string\[] | Chain IDs (e.g. `["100","1"]`). Empty = all. |
| `type` | `"Generic"` \| `"Futarchy"` \| `""` | Market type. |
| `parentMarket` | string (Address) | Filter by parent market. |
| `marketName` | string | Search by name. |
| `categoryList` | string\[] | Categories. |
| `marketStatusList` | MarketStatus\[] | Statuses (e.g. Active, Closed). |
| `verificationStatusList` | VerificationStatus\[] | Verification status. |
| `showConditionalMarkets` | boolean | Include conditional markets. |
| `showMarketsWithRewards` | boolean | Include markets with rewards. |
| `minLiquidity` | number | Minimum liquidity. |
| `creator` | string (Address) | Creator address. |
| `participant` | string (Address) | Participant address. |
| `orderBy` | Market\_OrderBy | Sort field (subgraph). |
| `orderDirection` | `"asc"` \| `"desc"` | Sort direction. |
| `marketIds` | string\[] | Explicit list of market IDs. |
| `limit` | number | Results per page (default 1000). |
| `page` | number | Page number (default 1). |

### Response 200

```json theme={null}
{
  "markets": [ /* SerializedMarket[] */ ],
  "count": number,
  "pages": number
}
```

### Errors

* **400** – `Missing request body`.
* **500** – `Internal server error`.

***

## get-token-transactions

Returns top holders and recent transactions for one or more tokens (per chain).

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-token-transactions` |
| **Parameters** | Query string |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `tokenIds` | Yes | Comma-separated list of token addresses. |
| `chainId` | Yes | Chain ID. Must be in `SUBGRAPHS.tokens`. |

### Response 200

```json theme={null}
{
  "topHolders": {
    "<tokenId>": [ { "address": string, "balance": string } ]
  },
  "recentTransactions": [ /* TokenTransfer[] – last 100 */ ],
  "totalTokens": number,
  "totalTransactions": number,
  "tokenIds": string[],
  "chainId": number
}
```

`recentTransactions` items use the following type (`value` is serialized as string in JSON):

```ts theme={null}
type TokenTransfer = {
  id: string;
  chain_id: number; // SupportedChain
  token: `0x${string}`;
  tx_hash: string;
  log_index: number;
  block_number: number;
  timestamp: number;
  value: string; // serialized bigint
  from: `0x${string}`;
  to: `0x${string}`;
};
```

Header: `Cache-Control: public, max-age=300` (5 minutes).

### Errors

* **400** – Missing `tokenIds` or `chainId`, invalid `chainId`, or unsupported chain.
* **500** – Internal server error.

***

## get-transactions

Transaction history for an account: swaps, liquidity (LP add/remove), and CTF events (split/merge/redeem). Always computed across all production chains and cached; optional filters apply in memory on the cached payload.

Covers the account **and the TradeExecutor contracts it owns**. Someone trading through an executor holds the outcome tokens inside that contract and the DEX indexes the swap with the executor as `recipient`, so querying the address alone returned only what the EOA did directly. Rows carry `sourceWallet`, which differs from the requested account when an executor produced them, and are deduped across the wallets by `eventId`. Executor discovery is best-effort: if it fails the response is the account's own rows, as before.

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-transactions` |
| **Parameters** | Query string |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `account` | Yes | Wallet address. |
| `chainId` | No | Chain ID (number) or `all`. Omit or `all` returns every chain. |
| `startTime` | No | Unix timestamp start (number). |
| `endTime` | No | Unix timestamp end (number). |
| `eventType` | No | `swap` \| `lp` \| `ctf`. If omitted, all event types are returned. |

### Response 200

Array of `TransactionData`, sorted by `timestamp` descending (then `blockNumber`). Each row includes `chainId`.

```ts theme={null}
type TransactionType = "split" | "merge" | "redeem" | "swap" | "lp" | "lp-burn";

interface TransactionData {
  marketName: string;
  marketId: string;
  type: TransactionType;
  blockNumber: number;
  chainId: number;
  collateral: `0x${string}`;
  collateralSymbol?: string;
  timestamp?: number;
  transactionHash?: string;
  /** Source row id, unique per event. */
  eventId?: string;
  /** Wallet the row was indexed under. Absent means the requested account. */
  sourceWallet?: `0x${string}`;

  // split/mint/merge
  amount?: string;
  payout?: string;

  // swap
  tokenIn?: string;
  tokenOut?: string;
  tokenInSymbol?: string;
  tokenOutSymbol?: string;
  amountIn?: string;
  amountOut?: string;
  price?: string;

  // lp
  token0?: string;
  token1?: string;
  token0Symbol?: string;
  token1Symbol?: string;
  amount0?: string;
  amount1?: string;
}
```

### Errors

* **400** – Missing `account`, invalid `chainId`/`eventType`, or non-numeric `startTime`/`endTime`.
* **500** – Internal server error.

***

## get-portfolio

Positions for an account: outcome token balances per market, with market data, outcome, collateral, and redeemed prices. Always computed across all production chains and cached; optional `chainId` filters the cached payload.

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-portfolio` |
| **Parameters** | Query string |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `account` | Yes | Wallet address. |
| `chainId` | No | Chain ID (number) or `all`. Omit or `all` returns every chain. |
| `collateralProfile` | No | Registered profile name (`default`, `circles`, …). Defaults to `default`. Filters positions to markets using that profile’s primary collateral. |

### Response 200

Array of `PortfolioPosition`:

```ts theme={null}
interface PortfolioPosition {
  tokenId: `0x${string}`;
  tokenIndex: number;
  marketId: `0x${string}`;
  marketName: string;
  marketStatus: string;
  tokenBalance: number;
  tokenValue: number;
  tokenPrice: number;
  outcome: string;
  chainId: number;
  collateralToken: `0x${string}`;
  parentMarketId?: `0x${string}`;
  parentMarketName?: string;
  parentOutcome?: string;
  redeemedPrice: number;
  marketFinalizeTs: number;
  outcomeImage?: string;
  isInvalidOutcome: boolean;
}
```

Only positions with balance > 0 are included; for closed markets, only those that qualify as winning per payout.

### Errors

* **400** – Missing `account`, or invalid `chainId`.
* **500** – Internal server error.

***

## get-portfolio-value

Current portfolio value and \~24h mark-to-market change (outcome positions only; same collateral filter as `get-portfolio`). Totals are **USD** (primary collateral × spot USD via DexScreener / on-chain sDAI·sUSDS, same source as the PnL leaderboard).

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-portfolio-value` |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `account` | Yes | Wallet address. |
| `chainId` | Yes | Chain ID (number), or `all` to sum USD across supported chains. |
| `collateralProfile` | No | Registered profile name (see `get-portfolio`). |

### Response 200

`currentPortfolioValue`, `historyPortfolioValue`, `historyTimestamp`, `delta`, `deltaPercent`, `unit: "USD"`.

***

## get-portfolio-pl

Period P/L for an account (outcome MTM, including what the account holds in LP positions, + router collateral legs − swap/LP cashflow). Adding or removing liquidity on its own does not move P/L; price moves and fees compounded into the position do. Fees accrued but not collected are not counted. **Generic markets only** — Futarchy is excluded so non-primary notional (PNK/GNO, …) is not mixed into the profile’s primary units. `get-portfolio` still lists Futarchy positions.

* **Global** (no `marketId` / `marketIds`): reads materialized rows from Supabase `pnl_leaderboard` (`app_id = all`). No live compute; miss → zeros. Values are **USD** (`pnl` = `pnl_usd`; other fields converted with `collateral_price_usd`). `chainId=all` sums USD across chains. Response includes `unit: "USD"`.
* **Market-scoped** (`marketId` or `marketIds`): computes live via `computePortfolioPlAllPeriods` in native collateral units. Futarchy ids → 404. `chainId=all` is not allowed.

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-portfolio-pl` |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `account` | Yes | Wallet address. |
| `chainId` | Yes | Chain ID (number), or `all` (global path only). |
| `period` | No | `1d` \| `1w` \| `1m` \| `all` (default `1d`). |
| `marketId` | No | Restrict to one market. |
| `marketIds` | No | Comma-separated market IDs (same chain). Provide `marketId` or `marketIds`, not both. |
| `collateralProfile` | No | Registered profile name (see `get-portfolio`). Market-scoped only. |
| `debug` | No | `1` or `true` for formula breakdown on market-scoped requests (ignored on the global path). |

`@seer-pm/sdk` `fetchPortfolioPnL` / `fetchPortfolioValue` send `collateralProfile` from `getActiveCollateralProfileName()` after `configureCollateral` — see [Collateral profiles](/developers/guides/collateral-profiles). `fetchPortfolioPnL` / `fetchPortfolioValue` accept `chainId: number | "all"`.

***

## get-pnl-leaderboard

Ranked wallet P/L for markets configured under Seer apps (`web/src/lib/apps.ts`). Data is **materialized** by `scheduled-refresh-pnl-leaderboard` / `refresh-pnl-leaderboard-background` into Supabase `pnl_leaderboard` (apply `web/supabase/sql/pnl_leaderboard.sql` first). The refresh considers every wallet with analytics activity (not a top-N by tx count) and rotates a stale/missing batch each run.

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-pnl-leaderboard` |
| **CORS** | `*` (dashboard / external UIs) |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `app` | No | `all` (protocol-wide: every market on the chain, including those not in any app) \| `foresight` \| `opportunity` \| `deepfund` \| `futarchy` (default `all`). |
| `chainId` | No | Chain id, or `all` to sum USD across chains (default Gnosis `100`). |
| `period` | No | `1d` \| `1w` \| `1m` \| `all` (default `all`). |
| `limit` / `offset` | No | Pagination (limit max 200). |
| `search` | No | Case-insensitive substring filter on username or address. A leading `@` is optional. Only stored Seer usernames are matched: ENS names and generated display names are not searchable. Username matching needs at least 3 characters and resolves at most 500 wallets; a shorter fragment still filters by address. |
| `sort` | No | `pnl` \| `volume` \| `roi` \| `markets` \| `score` (default `pnl`). |
| `dir` | No | `asc` \| `desc` (default `desc`). Null values (`roi` on dust capital, `score` below the gate) always sort last, in both directions. |
| `rankFor` | No | `0x`-prefixed wallet address. Returns `{ app, chainId, period, sort, dir, address, rank, total }` instead of leaderboard rows. |
| `breakdown` | No | `1` or `true` to include `scoreBreakdown` on every row — `{ method, score, tier, components, sampleShrink, inputs }`, where `components` values are 0-100 sub-scores rather than weighted points. |

All rankings use **USD** (`pnl_usd` from materialization: native collateral PnL × spot collateral USD). Per-chain filters still return USD so ranks stay comparable; `chainId=all` sums `pnl_usd` across chains. App filter `all` is independent of `SEER_APPS` market lists.
Each row includes an optional `username` when the wallet has chosen one. Usernames are opt-in, so its absence means only that the wallet never set one.

### Trader score

Each row carries `score` (0-100, one decimal) and `tier` (`Elite` ≥85, `Great` ≥70, `Good` ≥55, `Average` ≥40, `Weak` below). It is a weighted mean of five clamped-linear components, shrunk toward 50 afterwards:

| Component | Weight | Measure | 0 points | 100 points |
| - | - | - | - | - |
| `returns` | 25% | ROI = `(grossProfit − grossLoss) / scoredCapitalUsd` | −25% | +75% |
| `profitFactor` | 30% | gross profit / gross loss, over markets | 0.5 | 3.0 |
| `hitRate` | 15% | `hitRate − breakEvenHitRate`, in probability | −0.10 | +0.15 |
| `lossBurn` | 15% | gross loss / `scoredCapitalUsd` | 50% | 0% |
| `breadth` | 15% | `1 − bestMarket / grossProfit` | 0 (one market) | 0.7 |

Every ratio is taken over the **same market set**: those carrying more than \$1 of capital at risk. `scoredCapitalUsd` is the capital in exactly that set, which is why `returns` can differ slightly from the row's `roi` (whole-wallet).

`hitRate` scores the wallet against its own break-even rate — `avgLoss / (avgWin + avgLoss)`, from the realised per-market payoffs — rather than a fixed band, because a prediction market pays out inversely to the price paid and a fixed band marks down every long-shot strategy regardless of profitability. `scoreBreakdown.inputs` carries `hitRate`, `breakEvenHitRate` and their difference `hitEdge`; the component scores `hitEdge`.

`scoreBreakdown.sampleShrink` is `{ scoredMarketCount, factor, rawScore }`, where `factor` is `n / (n + 10)` and `score = 50 + (rawScore − 50) × factor`. The weighted component points sum to `rawScore`, **not** to `score` — a client rendering the breakdown has to show the shrink or it will look like an arithmetic error.

`score` is `null`, never 0, when the wallet has fewer than 3 markets above $1 of capital at risk, less than $100 of capital across those markets, or more than 25% of its `pnl_usd` outside them. When it is null the row also carries `scoreUnavailable` — `{ reason: "markets" | "capital" | "coverage", scoredMarketCount, minScoredMarkets, minCapitalUsd, dustUsd, unscoredPnlFraction, maxUnscoredPnlFraction }` — so a client can say which gate was missed. Note that `marketCount` counts every traded market while the gate counts only those above \$1 of capital at risk, so a wallet can show a large `marketCount` and still be unscored, or (for a pure LP wallet, which never swaps) a smaller one. Two priors keep small books honest on top of the shrink: the hit rate carries 5 pseudo-markets at 50%, and the profit factor carries symmetric prior mass of 2% of the book's own gross flow on both sides, so a book with no losing market reads a finite ratio rather than infinity.

`reason: "coverage"` means the scored markets do not account for the wallet's P/L. Positions that move value without putting primary collateral at risk — conditional markets, whose pool trades one outcome token against another, and outcome tokens received by transfer — carry P/L into `pnl_usd` against no measured capital, so they never enter the scored set. `pnl` and `roi` on the row still include them.

Read `score` as a summary of the wallet's **cross-market** distribution. It is not a time-series metric: `lossBurn` is a terminal-state loss measure — negative terminal market P/L over capital — and says nothing about the path taken to get there (it cannot see a position that was deep underwater mid-window and recovered), `breadth` measures concentration across markets rather than the smoothness of an equity curve, and `returns` has a capital denominator but no risk denominator. `scoreBreakdown.method` is `"markets"` and identifies which implementation produced the number.

The score is computed at read time from additive statistics stored on `pnl_leaderboard`, so it is correct after the executor→owner, app-scope and cross-chain merges. It is not stored per row, and it is not meaningful to average two scores.

Those statistics describe an **owner's** book, not an address's. A wallet that trades through a TradeExecutor contract holds its capital on one address and, after the contract sweeps the outcome tokens out, its value on another; each market would fall below the \$1 gate on both. So the per-market books of an owner and its executors are combined at materialization, before the gate, and the resulting statistics are stored on the owner's row while the executors' rows carry zeros. `market_count` follows them: it counts *distinct* markets, so summing each member's own count double-counted every market an owner and its executor both traded. The additive totals — `pnl_usd`, `volume_usd`, `capital_deployed` — do stay per address and still sum across the merge.

***

## get-market-pnl-leaderboard

Ranked wallet P/L within one market, including its conditional children. Reads `pnl_market_leaderboard` (apply `web/supabase/sql/pnl_market_leaderboard.sql` first) — same materialization pass as the global board, so freshness is identical.

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/get-market-pnl-leaderboard` |
| **CORS** | `*` |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `marketId` | Yes | Market address. Conditional children are expanded and aggregated in. |
| `chainId` | Yes | Chain id. `all` is not supported. |
| `period` | No | `1d` \| `1w` \| `1m` \| `all` (default `all`). |
| `limit` / `offset` | No | Pagination (limit max 100). |
| `search` | No | Hex address fragment. |
| `sort` | No | `pnl` \| `volume` \| `roi` \| `markets` (default `pnl`). **`score` is not accepted here** — see below. |
| `dir` | No | `asc` \| `desc` (default `desc`). |

### Response 200

`{ marketId, marketIds, chainId, period, total, rows }`, where each row is `{ rank, address, pnl, volume, roi, unit: "USD", capitalUsd, score, tier, updatedAt, mergedWallets? }`.

`score` and `tier` are the trader's **protocol-wide** score (the `app_id = 'all'` board for the same chain and period), not a score for this market: a single market can never clear the score's 3-market eligibility gate, so a market-scoped score would always be `null`. It is looked up for the current page only, after pagination — which is why this endpoint cannot rank by it. A failed lookup yields `score: null` rather than failing the board.

***

## markets-charts

Returns chart data (hourly price/volume) for markets. If IDs are provided, only those markets; if omitted, all cached chart data.

| Field | Detail |
| - | - |
| **Method** | `GET` |
| **URL** | `https://app.seer.pm/.netlify/functions/markets-charts` |
| **Parameters** | Query string |

### Query params

| Parameter | Required | Description |
| - | - | - |
| `ids` | No | Comma-separated list of market IDs (Address). If omitted, all cached charts are returned. IDs are validated with `isAddress`. |

### Response 200

Object keyed by `marketId` with each market’s chart data (array of series):

```ts theme={null}
// Value per marketId:
type ChartDataSeries = {
  name: string;
  type: string;
  data: number[][]; // [timestamp, value] pairs (e.g. [unixSec, price])
}[];

// Full response type:
type MarketsChartsResponse = Record<`0x${string}`, ChartDataSeries>;
```

### Errors

* **400** – `ids` was provided but no valid address: `No valid market IDs provided`.
* **500** – Supabase or internal error.

***

## Quick reference for AIs

| Function | Method | Key params | Typical use |
| - | - | - | - |
| get-market | POST | `chainId` + `id` or `url` | Single market page or API |
| markets-search | POST | Body with filters and `page`/`limit` | Listings, search, filters |
| get-token-transactions | GET | `tokenIds`, `chainId` | Token holders and activity |
| get-transactions | GET | `account`, opt. `chainId`/`all`, `eventType`, `startTime`, `endTime` | Wallet transaction history |
| get-portfolio | GET | `account`, opt. `chainId`/`all`, `collateralProfile` | Wallet positions (filtered by profile); each row has `chainId` and `sourceWallet` |
| get-portfolio-value | GET | `account`, `chainId` or `all`, opt. `collateralProfile` | Portfolio value + 24h delta (USD) |
| get-portfolio-identity | GET | `account` | The account plus the TradeExecutor contracts it owns |
| get-portfolio-pl | GET | `account`, `chainId` or `all`, opt. `collateralProfile`, `period`, `marketId` / `marketIds` | Period P/L (global path USD; market-scoped native) |
| get-pnl-leaderboard | GET | `app`, `chainId` or `all`, `period`, `sort` | App-scoped P/L rankings and trader score |
| get-market-pnl-leaderboard | GET | `marketId`, `chainId`, `period` | P/L rankings within one market |
| markets-charts | GET | opt. `ids` (market IDs) | Price/volume charts per market |

All responses are JSON. Errors return `{ "error": "message" }` with the appropriate HTTP status code.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.