Skip to content

Kalshi Search & Baskets

Discover, cluster, correlate, and assemble baskets across the live Kalshi prediction-market universe. These endpoints turn the open market set into a queryable database with three lenses — keyword search, embedding similarity, and time-series math — joinable in a single request, plus a composite layer that pushes portfolio construction (correlation-aware basket building, Kelly sizing, backtests) entirely server-side so a client can satisfy each high-level use case with one HTTP call.

Pair this surface with the Prediction Markets Agent for narrative analysis on individual events, and with Events History for per-event time series.

Like the Chat Completions and Responses endpoints, these are direct REST endpoints called relative to the Octagon API base URL:

text
https://api.octagonai.co/v1

Authentication

Every request requires a valid Octagon API key passed as a Bearer token. Requests without one return 401.

Authorization: Bearer your-octagon-api-key

Conventions

ConventionDetails
Path styleKebab-case (e.g. /behavioral-clusters, /markets-with-edge, /cluster-peers).
Field stylesnake_case in query strings, request bodies, and response payloads.
TimestampsRFC 3339 UTC (2026-08-19T00:00:00Z).
Prices0–1 fraction units (Kalshi cents are normalized server-side).
Paginationbase64(JSON) cursor tokens. Pass next_cursor from a previous page back as cursor=....
Unknown paramsReturn 400 with a list of unknown query parameter names.
Bad bodiesPydantic validation errors return 422 for missing or wrongly-typed fields.

Data model

The endpoints read from a set of tables maintained by the nightly Kalshi sync + clustering pipeline.

TablePurpose
kalshi_markets_activeLive market universe. Includes a 256-d pgvector embedding and a GIN-indexed search_tsv TSVECTOR for full-text search.
kalshi_events_activeEvent metadata, including category / subcategory.
kalshi_market_candles / kalshi_market_candles_dailyHourly and daily OHLC bars.
kalshi_clusters (+ assignments + runs)Thematic K-means clusters over market embeddings, LLM-labeled, atomically swapped per nightly run.
kalshi_behavioral_clusters (+ assignments + runs)Behavioral K-means clusters over 30-day daily return vectors. Covers only markets with ≥14 days of daily candles.
events_historyPer-run event snapshots — the same table that powers /prediction-markets/events. Drives /kalshi/markets-with-edge.

Primitives

Single-purpose endpoints that a client can compose freely.

GET /kalshi/markets

Structured + full-text search over the open Kalshi universe.

Query parameters

ParameterTypeRequiredDescription
qstringNoFull-text query against search_tsv (title + subtitle + ticker). Results are ranked by ts_rank when supplied. Minimum 3 characters.
categorystringNoMatches event category or subcategory (exact).
series_tickerstringNoFilter by Kalshi series.
event_tickerstringNoFilter to markets in a single event.
close_beforedatetimeNoOnly markets closing on or before this RFC 3339 timestamp.
min_volume_24hfloatNoFloor on volume_24h.
limitintegerNoPage size. Default 50; min 1; max 200.
cursorstringNoPagination cursor returned from a previous response.

Example

Python
import requests

url = "https://api.octagonai.co/v1/prediction-markets/kalshi/markets"
headers = {"Authorization": "Bearer your-octagon-api-key"}
params = {
    "q": "bitcoin",
    "category": "crypto",
    "min_volume_24h": 10000,
    "limit": 20,
}

response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
print(response.json())
JavaScript
const params = new URLSearchParams({
  q: "bitcoin",
  category: "crypto",
  min_volume_24h: "10000",
  limit: "20",
});

const response = await fetch(
  `https://api.octagonai.co/v1/prediction-markets/kalshi/markets?${params}`,
  { headers: { Authorization: "Bearer your-octagon-api-key" } }
);

if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.json());
sh
curl -G "https://api.octagonai.co/v1/prediction-markets/kalshi/markets" \
  -H "Authorization: Bearer your-octagon-api-key" \
  --data-urlencode "q=bitcoin" \
  --data-urlencode "category=crypto" \
  --data-urlencode "min_volume_24h=10000" \
  --data-urlencode "limit=20"

Example response

json
{
  "data": [
    {
      "market_ticker": "KXBTCD-26DEC31-T100000",
      "event_ticker": "KXBTCD-26DEC31",
      "series_ticker": "KXBTCD",
      "title": "Bitcoin above $100k by end of Dec 2026",
      "subtitle": null,
      "status": "open",
      "close_time": "2026-12-31T23:59:59Z",
      "last_price": 0.58,
      "yes_bid": 0.57,
      "yes_ask": 0.59,
      "no_bid": 0.41,
      "no_ask": 0.43,
      "volume": 1234,
      "volume_24h": 1234,
      "liquidity": 100,
      "open_interest": 5,
      "category": "crypto",
      "event_name": "Bitcoin price ladders"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

GET /kalshi/markets/similar

Cosine-distance similarity over kalshi_markets_active.embedding. Catches matches keyword search misses (e.g. "Will Bitcoin pierce six figures""BTC above $100k") and is freely combinable with structured filters.

Query parameters — exactly one of anchor_ticker or q is required.

ParameterTypeRequiredDescription
anchor_tickerstringCond.Use the stored embedding of this market as the anchor. Zero added latency. Mutually exclusive with q.
qstringCond.Anchor by free-text query. Embedded at request time via text-embedding-3-small at dim=256. Adds one OpenAI roundtrip. Mutually exclusive with anchor_ticker.
top_kintegerNoNumber of nearest neighbors. Default 25; min 1; max 100.
categorystringNoRestrict to a category.
min_volume_24hfloatNoFloor on volume_24h.
close_beforedatetimeNoOnly markets closing before this RFC 3339 timestamp.

Supplying neither or both of anchor_ticker / q returns 400.

Example — anchor by ticker (no OpenAI call)

Python
import requests

response = requests.get(
    "https://api.octagonai.co/v1/prediction-markets/kalshi/markets/similar",
    headers={"Authorization": "Bearer your-octagon-api-key"},
    params={"anchor_ticker": "KXBTCD-26DEC31-T100000", "top_k": 10},
)
response.raise_for_status()
print(response.json())
JavaScript
const params = new URLSearchParams({
  anchor_ticker: "KXBTCD-26DEC31-T100000",
  top_k: "10",
});

const response = await fetch(
  `https://api.octagonai.co/v1/prediction-markets/kalshi/markets/similar?${params}`,
  { headers: { Authorization: "Bearer your-octagon-api-key" } }
);
console.log(await response.json());
sh
curl -G "https://api.octagonai.co/v1/prediction-markets/kalshi/markets/similar" \
  -H "Authorization: Bearer your-octagon-api-key" \
  --data-urlencode "anchor_ticker=KXBTCD-26DEC31-T100000" \
  --data-urlencode "top_k=10"

Example — anchor by free-text query (embedded server-side)

sh
curl -G "https://api.octagonai.co/v1/prediction-markets/kalshi/markets/similar" \
  -H "Authorization: Bearer your-octagon-api-key" \
  --data-urlencode "q=Will Bitcoin pierce six figures" \
  --data-urlencode "category=crypto" \
  --data-urlencode "min_volume_24h=10000"

Example response

json
{
  "anchor_ticker": "KXBTCD-26DEC31-T100000",
  "anchor_query": null,
  "data": [
    {
      "market_ticker": "KXETHU-26DEC31-T10000",
      "event_ticker": "KXETHU-26DEC31",
      "title": "ETH above $10k by Dec 2026",
      "category": "crypto",
      "distance": 0.18
    }
  ]
}

Lower distance = closer cosine similarity.

GET /kalshi/clusters

Browse the current run of thematic (embedding-based) clusters. Each cluster has an LLM-generated label and description, the cluster size, and a few representative sample_titles.

Query parameters

ParameterTypeRequiredDescription
limitintegerNoDefault 200; min 1; max 500.
sample_titlesintegerNoNumber of sample titles per cluster. Default 4; min 0; max 20.
label_containsstringNoCase-insensitive substring filter on the cluster label.

Example

sh
curl -G "https://api.octagonai.co/v1/prediction-markets/kalshi/clusters" \
  -H "Authorization: Bearer your-octagon-api-key" \
  --data-urlencode "label_contains=fed" \
  --data-urlencode "sample_titles=4"

Example response

json
{
  "data": [
    {
      "cluster_id": 42,
      "label": "Fed decisions",
      "description": "Markets resolving on FOMC actions and Fed policy outcomes",
      "size": 14,
      "sample_titles": [
        "Will the Fed hike in March?",
        "FOMC March: 25bp cut?"
      ],
      "created_at": "2026-05-21T03:00:00Z"
    }
  ]
}

GET /kalshi/clusters/{cluster_id}/markets

Markets in one thematic cluster, ranked ascending by distance to the cluster centroid. Same response shape and cursor pagination as GET /kalshi/markets.

Example

sh
curl "https://api.octagonai.co/v1/prediction-markets/kalshi/clusters/42/markets?limit=20" \
  -H "Authorization: Bearer your-octagon-api-key"

GET /kalshi/behavioral-clusters

Same shape as /kalshi/clusters but over the behavioral cluster run — K-means on 30-day daily return vectors. Each row additionally exposes mean_daily_return and daily_volatility. Covers only the subset of markets with ≥14 days of daily candles (the threshold for behavioral eligibility).

Query parameters — identical to GET /kalshi/clusters.

Example

sh
curl "https://api.octagonai.co/v1/prediction-markets/kalshi/behavioral-clusters?limit=40" \
  -H "Authorization: Bearer your-octagon-api-key"

GET /kalshi/behavioral-clusters/{cluster_id}/markets

Members of one behavioral cluster, same response shape as the thematic version.

sh
curl "https://api.octagonai.co/v1/prediction-markets/kalshi/behavioral-clusters/4/markets?limit=20" \
  -H "Authorization: Bearer your-octagon-api-key"

GET /kalshi/markets/{market_ticker}/clusters

Return the thematic and behavioral cluster IDs a single market belongs to under the current runs. Useful for driving "no more than N legs from the same cluster" basket constraints client-side when not using /kalshi/baskets/build.

Example

sh
curl "https://api.octagonai.co/v1/prediction-markets/kalshi/markets/KXBTCD-26DEC31-T100000/clusters" \
  -H "Authorization: Bearer your-octagon-api-key"

Example response

json
{
  "market_ticker": "KXBTCD-26DEC31-T100000",
  "thematic": {
    "cluster_id": 17,
    "label": "Crypto price ladders",
    "description": "BTC/ETH/SOL strike-ladder markets",
    "size": 122
  },
  "behavioral": {
    "cluster_id": 4,
    "label": "Mean-reverting low-vol",
    "size": 87,
    "mean_daily_return": -0.002,
    "daily_volatility": 0.018
  }
}

Either field can be null if the market isn't in a current-run assignment for that clustering kind.

POST /kalshi/markets/correlations

Pairwise Pearson correlation matrix over close-price series for N markets.

Request body

FieldTypeRequiredDescription
market_tickersarray of stringsYes2–100 distinct market tickers.
window_daysintegerYesLookback window. Min 1, max 730.
intervalstringNo"1h" or "1d". Auto-picked when omitted: 1d when window_days ≥ 90, else 1h. 1h reads hourly candles, 1d reads daily.

Example

sh
curl -X POST "https://api.octagonai.co/v1/prediction-markets/kalshi/markets/correlations" \
  -H "Authorization: Bearer your-octagon-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "market_tickers": ["KXBTCD-26DEC31-T100000", "KXETHU-26DEC31-T10000", "KXSOL-26DEC31-T200"],
    "window_days": 90,
    "interval": "1d"
  }'

Example response

json
{
  "tickers": ["KXBTCD-26DEC31-T100000", "KXETHU-26DEC31-T10000", "KXSOL-26DEC31-T200"],
  "matrix": [
    [1.0, 0.92, 0.81],
    [0.92, 1.0, 0.74],
    [0.81, 0.74, 1.0]
  ],
  "ranked_pairs": [
    { "ticker_a": "KXBTCD-26DEC31-T100000", "ticker_b": "KXSOL-26DEC31-T200",   "correlation": 0.81 },
    { "ticker_a": "KXETHU-26DEC31-T10000",  "ticker_b": "KXSOL-26DEC31-T200",   "correlation": 0.74 },
    { "ticker_a": "KXBTCD-26DEC31-T100000", "ticker_b": "KXETHU-26DEC31-T10000", "correlation": 0.92 }
  ],
  "window_days": 90,
  "interval": "1d",
  "missing": []
}
  • ranked_pairs is the upper-triangle of the matrix sorted ascending by correlation — the most-uncorrelated pairs come first.
  • Markets without candle data in the window appear in missing and are dropped from the matrix.
  • Cells with fewer than 3 paired observations or constant series come back as null.

POST /kalshi/baskets/candles

OHLC bars for a weighted basket NAV.

Request body

FieldTypeRequiredDescription
market_tickersarray of stringsYesTickers to combine into the basket.
weightsarray of numbersNoPer-ticker weights. Defaults to equal weight. Must match market_tickers length and sum to a positive value (renormalized server-side).
timeframestringYesOne of 1w, 1m, 3m, 6m, 1y. Drives both the lookback window and the candle bin size (see below).
TimeframeLookbackBin
1w7 days3h
1m30 days12h
3m90 days1d
6m180 days3d
1y365 days7d

Example

sh
curl -X POST "https://api.octagonai.co/v1/prediction-markets/kalshi/baskets/candles" \
  -H "Authorization: Bearer your-octagon-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "market_tickers": ["KXBTCD-26DEC31-T100000", "KXETHU-26DEC31-T10000"],
    "weights": [0.6, 0.4],
    "timeframe": "1y"
  }'

Example response

json
{
  "timeframe": "1y",
  "interval_source": "1d",
  "candles": [
    { "time": 1735776000, "open": 0.41, "high": 0.45, "low": 0.40, "close": 0.44 }
  ],
  "tickers": ["KXBTCD-26DEC31-T100000", "KXETHU-26DEC31-T10000"],
  "missing": []
}

time values are Unix-seconds boundaries for each bin. Markets without data in the window appear in missing and are excluded from the basket NAV.


Composite endpoints

Higher-level endpoints that combine the primitives plus portfolio math server-side so a client can satisfy a use case in a single request.

GET /kalshi/markets/{market_ticker}/cluster-peers

One-call "show me others in the same theme" — replaces the two-step GET /markets/{ticker}/clustersGET /clusters/{id}/markets dance.

Query parameters

ParameterTypeRequiredDescription
kindstringNothematic (default) or behavioral. Selects the clustering to use.
limitintegerNoNumber of peers, excluding the anchor. Default 50; min 1; max 200.

Example

sh
curl "https://api.octagonai.co/v1/prediction-markets/kalshi/markets/KXBTCD-26DEC31-T100000/cluster-peers?kind=thematic&limit=50" \
  -H "Authorization: Bearer your-octagon-api-key"

Example response

json
{
  "market_ticker": "KXBTCD-26DEC31-T100000",
  "kind": "thematic",
  "cluster": {
    "cluster_id": 17,
    "label": "Crypto price ladders",
    "description": "BTC/ETH/SOL strike-ladder markets",
    "size": 122
  },
  "data": [
    { "market_ticker": "KXETHU-26DEC31-T10000", "title": "ETH above $10k by Dec 2026", "distance": 0.04 }
  ]
}

POST /kalshi/baskets/size

Apply fractional Kelly to a set of legs the caller has already picked. The server looks up each leg's live yes_bid / no_bid from kalshi_markets_active to determine entry price.

Kelly formula (binary outcome)

For a leg with entry price p (the bid for the chosen side) and model probability q:

edge_pp   = (q - p) * 100
payoff b  = (1 - p) / p
fraction  = (b * q - (1 - q)) / b    # classic Kelly
f_capped  = min(max(fraction, 0), kelly_multiplier)

Sums of f_capped are scaled down so total allocation stays within the multiplier cap. Legs with no edge (q < p) get kelly_fraction = 0.

Request body

FieldTypeRequiredDescription
bankroll_usdnumberYesMust be > 0.
kelly_multipliernumberYes0 ≤ x ≤ 1. Cap on total bankroll fraction (e.g. 0.25 for "quarter Kelly").
legsarray of objectsYes1–50 entries, each { "market_ticker": string, "side": "yes" | "no", "model_probability": number }.

Example

sh
curl -X POST "https://api.octagonai.co/v1/prediction-markets/kalshi/baskets/size" \
  -H "Authorization: Bearer your-octagon-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "bankroll_usd": 1000.0,
    "kelly_multiplier": 0.25,
    "legs": [
      { "market_ticker": "KX-A", "side": "yes", "model_probability": 0.62 },
      { "market_ticker": "KX-B", "side": "no",  "model_probability": 0.55 }
    ]
  }'

Example response

json
{
  "bankroll_usd": 1000.0,
  "kelly_multiplier": 0.25,
  "total_notional": 168.50,
  "legs": [
    {
      "market_ticker": "KX-A",
      "side": "yes",
      "model_probability": 0.62,
      "price": 0.55,
      "edge_pp": 7.0,
      "kelly_fraction": 0.127,
      "weight": 0.62,
      "notional_usd": 104.45
    }
  ]
}

POST /kalshi/baskets/build

One-shot diversified basket builder. Pulls a candidate universe, annotates each candidate with its thematic cluster, computes correlations across the pool, greedily selects legs that respect both a per-cluster cap and a pairwise correlation cap, then sizes the legs by the chosen strategy.

Request body

FieldTypeRequiredDescription
universeobjectYesFilters that define the candidate pool. See below.
nintegerYesNumber of legs requested. Min 1; max 20.
max_per_clusterintegerYesCaps how many legs may share a single thematic cluster. Min 1; max 20.
max_pairwise_correlationnumberYesRange -1.0 to 1.0. Rejects any candidate whose worst pairwise correlation with already-accepted legs exceeds this value.
candidate_pool_sizeintegerYesHow many markets to consider before greedy selection. Min 2; max 200.
correlation_window_daysintegerYesLookback for the correlation matrix used during selection. Min 7; max 365. Interval auto-picks 1d for ≥90 days, else 1h.
sizingobjectYes{ "strategy": "equal" } or { "strategy": "kelly", "bankroll_usd": ..., "kelly_multiplier": ..., "leg_probabilities": { "<market_ticker>": <probability> } }. Missing probabilities default to 0.5 (no edge → zero allocation).

universe fields

FieldTypeDescription
qstringFree-text query — when set, the pool comes from semantic similarity via find_similar_markets.
anchor_tickerstringSemantic anchor — same effect as q, but no embedding roundtrip.
categorystringRestrict to a category.
series_tickerstringRestrict to a series.
min_volume_24hnumberFloor on volume_24h.
close_beforedatetimeOnly markets closing before this RFC 3339 timestamp.
label_contains_anyarrayRestrict to markets whose thematic cluster label matches any of these substrings (e.g. ["fed", "cpi"]).

When q / anchor_ticker is unset, the pool is structured-only via search_active_markets.

Example

Python
import requests

response = requests.post(
    "https://api.octagonai.co/v1/prediction-markets/kalshi/baskets/build",
    headers={
        "Authorization": "Bearer your-octagon-api-key",
        "Content-Type": "application/json",
    },
    json={
        "universe": {"category": "crypto", "min_volume_24h": 10000},
        "n": 8,
        "max_per_cluster": 2,
        "max_pairwise_correlation": 0.6,
        "candidate_pool_size": 50,
        "correlation_window_days": 60,
        "sizing": {
            "strategy": "kelly",
            "bankroll_usd": 1000,
            "kelly_multiplier": 0.25,
            "leg_probabilities": {
                "KXBTCD-26DEC31-T100000": 0.62,
                "KXETHU-26DEC31-T10000": 0.58,
            },
        },
    },
)
response.raise_for_status()
print(response.json())
JavaScript
const response = await fetch(
  "https://api.octagonai.co/v1/prediction-markets/kalshi/baskets/build",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer your-octagon-api-key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      universe: { category: "crypto", min_volume_24h: 10000 },
      n: 8,
      max_per_cluster: 2,
      max_pairwise_correlation: 0.6,
      candidate_pool_size: 50,
      correlation_window_days: 60,
      sizing: {
        strategy: "kelly",
        bankroll_usd: 1000,
        kelly_multiplier: 0.25,
        leg_probabilities: {
          "KXBTCD-26DEC31-T100000": 0.62,
          "KXETHU-26DEC31-T10000": 0.58,
        },
      },
    }),
  }
);

if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.json());
sh
curl -X POST "https://api.octagonai.co/v1/prediction-markets/kalshi/baskets/build" \
  -H "Authorization: Bearer your-octagon-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "universe": {"category": "crypto", "min_volume_24h": 10000},
    "n": 8,
    "max_per_cluster": 2,
    "max_pairwise_correlation": 0.6,
    "candidate_pool_size": 50,
    "correlation_window_days": 60,
    "sizing": {
      "strategy": "kelly",
      "bankroll_usd": 1000,
      "kelly_multiplier": 0.25,
      "leg_probabilities": {
        "KXBTCD-26DEC31-T100000": 0.62,
        "KXETHU-26DEC31-T10000": 0.58
      }
    }
  }'

Example response

json
{
  "legs": [
    {
      "market_ticker": "KXBTCD-26DEC31-T100000",
      "title": "Bitcoin above $100k by end of Dec 2026",
      "category": "crypto",
      "cluster_id": 17,
      "cluster_label": "Crypto price ladders",
      "volume_24h": 50000,
      "price": 0.58,
      "side": "yes",
      "model_probability": 0.62,
      "kelly_fraction": 0.08,
      "weight": 0.18,
      "notional_usd": 76.40
    }
  ],
  "realized_max_pairwise_correlation": 0.54,
  "cluster_breakdown": { "17": 2, "22": 2, "31": 1, "44": 1, "58": 2 },
  "dropped": [
    { "market_ticker": "KX-X", "reason": "cluster_cap_reached" },
    { "market_ticker": "KX-Y", "reason": "correlation_above_threshold" }
  ],
  "universe_size": 50
}

realized_max_pairwise_correlation lets the caller verify the constraint was satisfied; dropped shows which candidates were rejected and why.

POST /kalshi/baskets/backtest

Superset of /kalshi/baskets/candles. Returns the same OHLC bars plus a summary block with backtest statistics computed over the basket NAV series. Request body is identical to /kalshi/baskets/candles.

Annualization details

Kalshi markets trade 24/7, so we annualize by calendar seconds rather than 252 trading days. The basket-candles bin size determines periods_per_year:

TimeframeBinperiods_per_year
1w3h~2922
1m12h~731
3m1d~365
6m3d~122
1y7d~52
  • sharpe = (mean_period_return / std_period_return) * sqrt(periods_per_year) when std > 0, else null.
  • annualized_return = (1 + total_return) ** (periods_per_year / n_returns) - 1.
  • max_drawdown is reported as a negative fraction (e.g. -0.092 = 9.2% drawdown from peak).
  • win_rate is the share of bin-to-bin returns that were positive.

Example

sh
curl -X POST "https://api.octagonai.co/v1/prediction-markets/kalshi/baskets/backtest" \
  -H "Authorization: Bearer your-octagon-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "market_tickers": ["KX-A","KX-B","KX-C"],
    "weights": [0.4, 0.4, 0.2],
    "timeframe": "1y"
  }'

Example response

json
{
  "timeframe": "1y",
  "interval_source": "1d",
  "candles": [],
  "tickers": ["KX-A", "KX-B", "KX-C"],
  "missing": [],
  "summary": {
    "total_return": 0.247,
    "annualized_return": 0.247,
    "sharpe": 1.21,
    "max_drawdown": -0.092,
    "win_rate": 0.55,
    "first_nav": 0.40,
    "final_nav": 0.50,
    "observation_count": 52
  }
}

GET /kalshi/clusters/ranked-by-return

"Show me thematic baskets that historically returned ≥ X%" in one call. For each current cluster, picks the top-N markets by volume_24h, builds an equal-weight basket, runs the backtest, and returns clusters whose total_return ≥ min_return, sorted descending by return.

Query parameters

ParameterTypeRequiredDescription
timeframestringNoOne of 1w, 1m, 3m, 6m, 1y (default 1y).
min_returnnumberNoMinimum total_return to include. Default 0.0.
top_n_per_clusterintegerNoBasket size per cluster. Default 5; min 1; max 20.
kindstringNothematic (default) or behavioral.
max_clustersintegerNoCap on how many clusters to evaluate (cost control). Default 50; min 1; max 200.

Example

sh
curl -G "https://api.octagonai.co/v1/prediction-markets/kalshi/clusters/ranked-by-return" \
  -H "Authorization: Bearer your-octagon-api-key" \
  --data-urlencode "timeframe=1y" \
  --data-urlencode "min_return=0.20" \
  --data-urlencode "top_n_per_cluster=5"

Example response

json
{
  "timeframe": "1y",
  "kind": "thematic",
  "top_n_per_cluster": 5,
  "min_return": 0.20,
  "data": [
    {
      "cluster_id": 22,
      "label": "AI infrastructure",
      "description": "Markets resolving on AI infra capex, datacenter buildouts, and GPU shipments",
      "size": 18,
      "basket_tickers": ["KX-AI1", "KX-AI2", "KX-AI3", "KX-AI4", "KX-AI5"],
      "summary": {
        "total_return": 0.41,
        "sharpe": 1.6,
        "max_drawdown": -0.11
      }
    }
  ]
}

GET /kalshi/markets-with-edge

Edge-vs-consensus ranking joined to the latest completed run from events_history (the same table that powers GET /prediction-markets/events). One-call recipe for "10 most-traded politics markets ranked by edge vs consensus".

Query parameters

ParameterTypeRequiredDescription
run_idstring (UUID)NoDefaults to the most recent events_history.run_id.
categorystringNoFilter on series_category (case-insensitive).
edge_pp_minnumberNoLower bound on edge_pp (model probability minus market probability, in percentage points).
edge_pp_maxnumberNoUpper bound on edge_pp.
expected_return_minnumberNoFloor on expected_return.
total_volume_minnumberNoFloor on total_volume.
model_probability_minnumberNoFloor on model_probability.
sort_bystringNoOne of edge_pp (default), expected_return, total_volume, model_probability. Descending.
limitintegerNoDefault 50; min 1; max 200.
cursorstringNoPagination cursor.

Example

Python
import requests

response = requests.get(
    "https://api.octagonai.co/v1/prediction-markets/kalshi/markets-with-edge",
    headers={"Authorization": "Bearer your-octagon-api-key"},
    params={"category": "politics", "sort_by": "edge_pp", "limit": 10},
)
response.raise_for_status()
print(response.json())
JavaScript
const params = new URLSearchParams({
  category: "politics",
  sort_by: "edge_pp",
  limit: "10",
});

const response = await fetch(
  `https://api.octagonai.co/v1/prediction-markets/kalshi/markets-with-edge?${params}`,
  { headers: { Authorization: "Bearer your-octagon-api-key" } }
);
console.log(await response.json());
sh
curl -G "https://api.octagonai.co/v1/prediction-markets/kalshi/markets-with-edge" \
  -H "Authorization: Bearer your-octagon-api-key" \
  --data-urlencode "category=politics" \
  --data-urlencode "sort_by=edge_pp" \
  --data-urlencode "limit=10"

Example response

json
{
  "run_id": "8c0e1b9a-1f44-4d2a-9b7e-2d6e5f50a912",
  "captured_at": "2026-05-21T03:00:00Z",
  "sort_by": "edge_pp",
  "data": [
    {
      "event_ticker": "KXFEDCUT-26JUN",
      "title": "Will the Fed cut rates in June?",
      "series_category": "politics",
      "model_probability": 0.62,
      "market_probability": 0.55,
      "edge_pp": 7.0,
      "expected_return": 0.12,
      "confidence_score": 0.8,
      "total_volume": 124300,
      "total_open_interest": 5400
    }
  ],
  "next_cursor": null,
  "has_more": false
}

If filters reject every row but the run exists, captured_at still reflects the run's snapshot age.


Use-case recipes

Each recipe is satisfied by a single HTTP call unless explicitly noted. All commands assume BASE=https://api.octagonai.co and a valid OCTAGON_API_KEY.

1. Keyword search — markets matching "BTC 100k"

sh
curl -G "$BASE/v1/prediction-markets/kalshi/markets" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "q=BTC 100k"

2. Semantic by ticker — markets similar to a known ticker

sh
curl "$BASE/v1/prediction-markets/kalshi/markets/similar?anchor_ticker=KXBTCD-26DEC31-T100000&top_k=25" \
  -H "Authorization: Bearer $OCTAGON_API_KEY"

3. Semantic by free text — "Will Bitcoin pierce six figures" finds "BTC over $100k"

sh
curl -G "$BASE/v1/prediction-markets/kalshi/markets/similar" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "q=Will Bitcoin pierce six figures" \
  --data-urlencode "top_k=25"

4. Filtered semantic — crypto markets thematically similar to ETH 2.0 staking, closing within 90 days, volume_24h > $10k

sh
curl -G "$BASE/v1/prediction-markets/kalshi/markets/similar" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "q=ETH 2.0 staking" \
  --data-urlencode "category=crypto" \
  --data-urlencode "close_before=2026-08-19T00:00:00Z" \
  --data-urlencode "min_volume_24h=10000"

5. Browse themes — clustered map of the universe

sh
curl "$BASE/v1/prediction-markets/kalshi/clusters?limit=40&sample_titles=4" \
  -H "Authorization: Bearer $OCTAGON_API_KEY"

Render label, description, size, sample_titles as a card grid. Drill in with /kalshi/clusters/{id}/markets.

6. Cluster browsing — "all markets in the Fed-decisions theme" (two calls)

sh
# Step 1: find the cluster ID by label
curl -G "$BASE/v1/prediction-markets/kalshi/clusters" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "label_contains=fed"

# Step 2: list the markets in it
curl "$BASE/v1/prediction-markets/kalshi/clusters/42/markets?limit=50" \
  -H "Authorization: Bearer $OCTAGON_API_KEY"

7. Fast first-pass dedup — "show me others in the same theme"

sh
curl "$BASE/v1/prediction-markets/kalshi/markets/KXBTCD-26DEC31-T100000/cluster-peers?kind=thematic&limit=50" \
  -H "Authorization: Bearer $OCTAGON_API_KEY"

8. Pairwise correlation — find uncorrelated bets

sh
curl -X POST "$BASE/v1/prediction-markets/kalshi/markets/correlations" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"market_tickers":["KXBTCD-26DEC31-T100000","KXETHU-26DEC31-T10000","KXSOL-26DEC31-T200"],"window_days":90,"interval":"1d"}'

Read off ranked_pairs[0..k] for the most-uncorrelated pairs — no client-side sorting needed.

9. "Find me 5 uncorrelated bets on macro themes"

sh
curl -X POST "$BASE/v1/prediction-markets/kalshi/baskets/build" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "universe": {"label_contains_any": ["fed", "cpi", "fomc", "gdp", "jobs"]},
    "n": 5,
    "max_per_cluster": 1,
    "max_pairwise_correlation": 0.4,
    "candidate_pool_size": 50,
    "correlation_window_days": 90,
    "sizing": {"strategy": "equal"}
  }'

10. "Build a $1000 basket on crypto"

sh
curl -X POST "$BASE/v1/prediction-markets/kalshi/baskets/build" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "universe": {"category": "crypto", "min_volume_24h": 10000},
    "n": 8,
    "max_per_cluster": 2,
    "max_pairwise_correlation": 0.6,
    "candidate_pool_size": 50,
    "correlation_window_days": 60,
    "sizing": {
      "strategy": "kelly",
      "bankroll_usd": 1000,
      "kelly_multiplier": 0.25,
      "leg_probabilities": {"KXBTCD-26DEC31-T100000": 0.62, "KXETHU-26DEC31-T10000": 0.58}
    }
  }'

leg_probabilities come from your edge engine. If you want Octagon-model probabilities, pull them from /kalshi/markets-with-edge first and feed them in.

11. "10 most-traded politics markets ranked by edge vs consensus"

sh
curl -G "$BASE/v1/prediction-markets/kalshi/markets-with-edge" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "category=politics" \
  --data-urlencode "sort_by=edge_pp" \
  --data-urlencode "limit=10"

Swap sort_by=total_volume to literally rank by trading activity first.

12. "Thematic baskets that historically returned 20%+"

sh
curl -G "$BASE/v1/prediction-markets/kalshi/clusters/ranked-by-return" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "timeframe=1y" \
  --data-urlencode "min_return=0.20" \
  --data-urlencode "top_n_per_cluster=5"

13. "Did this basket return 20%+?"

sh
curl -X POST "$BASE/v1/prediction-markets/kalshi/baskets/backtest" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"market_tickers":["KX-A","KX-B","KX-C"],"weights":[0.4,0.4,0.2],"timeframe":"1y"}'

Read summary.total_return directly.


What the client owns

The API is intentionally permissive — these are the bits we don't (and can't) absorb server-side:

  • Model probabilities for Kelly sizing. The client supplies them (typically pulled from /kalshi/markets-with-edge or from its own model). The Kelly endpoint never invents probabilities.
  • Order placement. Octagon doesn't trade your basket; that's a Kalshi-API concern.
  • Presentation. Chart rendering, table layout, and natural-language framing of results.

Errors

StatusCause
400Bad query parameters — unknown name, invalid kind / timeframe / sort_by, weights/tickers length mismatch, anchor_ticker + q both supplied, negative kelly_multiplier, and similar.
401Missing or invalid Authorization header.
422Pydantic validation failure on a request body (missing or wrongly-typed fields).
502Upstream failure — typically an OpenAI embedding hiccup on GET /kalshi/markets/similar?q=..., or a candle-pipeline query error. The detail carries only the exception class name; full diagnostics are in server logs.
503Kalshi search is unavailable — either kalshi_markets_active isn't populated yet (nightly sync job hasn't run) or q is shorter than 3 characters. The endpoint deliberately does not fall back to the legacy in-memory Kalshi-API scan, which was both slow and stale; resolving silently hid real problems. Confirm the sync schedule is enabled and the table has rows.

All errors return a JSON body with a detail field describing the cause.