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 (exact match).
series_prefixstringNoLIKE '<prefix>%' filter on series_ticker — tree-style browsing (e.g. KXBTC matches KXBTCD, KXBTCY, KXBTCMAX100, …).
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.
sort_bystringNovolume_24h, close_time, or last_price. Overrides the default (rank when q, otherwise volume_24h) so you can take a true top-N across the entire universe without client-side reranking.
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/series

Per-series rollup over kalshi_markets_active — one row per series with active counts, volume, and dominant category. Replaces "paginate /kalshi/markets and reduce client-side" (10–30 calls) with a single request, and gives you a series-level entry point into the universe tree.

Query parameters

ParameterTypeRequiredDescription
series_prefixstringNoLIKE '<prefix>%' filter on series_ticker (e.g. KXBTC matches KXBTCD, KXBTCY, KXBTCMAX100, …).
categorystringNoFilter against the dominant category for each series.
min_volume_24hfloatNoFloor on total_volume_24h.
sort_bystringNototal_volume_24h (default), market_count, or active_count.
limitintegerNoDefault 50; min 1; max 200.
cursorstringNoPagination cursor.

Example

sh
curl -G "https://api.octagonai.co/v1/prediction-markets/kalshi/series" \
  -H "Authorization: Bearer your-octagon-api-key" \
  --data-urlencode "series_prefix=KXBTC" \
  --data-urlencode "sort_by=total_volume_24h"

Example response

json
{
  "data": [
    {
      "series_ticker": "KXBTCD",
      "series_title": "Bitcoin Daily",
      "market_count": 42,
      "active_count": 35,
      "total_volume_24h": 1234567.89,
      "dominant_category": "Crypto",
      "categories": ["Crypto"],
      "last_seen_at": "2026-05-25T19:39:00Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Performance

This endpoint runs a full-aggregate GROUP BY over the active universe. Expect a 1–3 s first call until the rollup is materialized as part of the nightly sync.

GET /kalshi/series/{series_ticker}/events

List events inside a single series — replaces the manual q="IPO 2026" keyword-search step when you already know the series.

Query parameters

ParameterTypeRequiredDescription
qstringNoOptional free-text filter within the series (title / subtitle).
limitintegerNoDefault 50; min 1; max 200.
cursorstringNoPagination cursor.

Example

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

Example response

json
{
  "series_ticker": "KXIPO",
  "data": [
    {
      "event_ticker": "KXIPOSPACEX",
      "title": "When will SpaceX IPO?",
      "category": "Companies",
      "close_time": "2027-01-01T00:00:00Z",
      "market_count": 8,
      "active_market_count": 6,
      "total_volume_24h": 41230.50
    }
  ],
  "next_cursor": null,
  "has_more": false
}

GET /kalshi/events/{event_ticker}/markets

Walk an event's child markets — e.g. every strike in an IPO date-ladder, every state in an election, every band in a recession-by-date event. Removes the manual "search for event then filter" step.

Query parameters

ParameterTypeRequiredDescription
min_volume_24hfloatNoFloor on volume_24h.
limitintegerNoDefault 100; min 1; max 500.
cursorstringNoPagination cursor.

Example

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

Example response

json
{
  "event_ticker": "KXIPOSPACEX",
  "data": [
    {
      "market_ticker": "KXIPOSPACEX-26JUN01",
      "title": "SpaceX IPO before June 2026?",
      "status": "open",
      "yes_bid": 0.12,
      "no_bid": 0.86,
      "last_price": 0.13,
      "volume_24h": 2300,
      "close_time": "2026-06-01T00:00:00Z",
      "category": "Companies"
    }
  ],
  "next_cursor": null,
  "has_more": false
}

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. Side-aware: pass per-ticker sides to mix YES and NO legs without re-pulling candles (we flip the sign for each NO leg, since corr(YES_A, NO_B) = -corr(YES_A, YES_B)).

Request body

FieldTypeRequiredDescription
market_tickersarray of stringsYes2–100 distinct market tickers.
sidesarray of stringsNoPer-ticker side — "yes" or "no", same length as market_tickers. Defaults to all yes. Filtered to the present tickers in the response.
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.
include_cell_detailbooleanNoWhen true, include a cells_detail array with overlap counts and a reason code per pair. Default false.

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"],
    "sides": ["yes", "yes", "no"],
    "window_days": 90,
    "interval": "1d",
    "include_cell_detail": true
  }'

Example response

json
{
  "tickers": ["KXBTCD-26DEC31-T100000", "KXETHU-26DEC31-T10000", "KXSOL-26DEC31-T200"],
  "sides": ["yes", "yes", "no"],
  "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 }
  ],
  "cells_detail": [
    { "ticker_a": "KXBTCD-26DEC31-T100000", "ticker_b": "KXETHU-26DEC31-T10000", "correlation": 0.92,  "overlap_count": 720, "reason": "ok" },
    { "ticker_a": "KXBTCD-26DEC31-T100000", "ticker_b": "KXSOL-26DEC31-T200",    "correlation": -0.81, "overlap_count": 720, "reason": "ok" },
    { "ticker_a": "KXETHU-26DEC31-T10000",  "ticker_b": "KXSOL-26DEC31-T200",    "correlation": -0.74, "overlap_count": 720, "reason": "ok" }
  ],
  "window_days": 90,
  "interval": "1d",
  "missing": []
}
  • matrix cells are side-flipped server-side using the supplied sides (or all yes when omitted) — no client-side sign adjustment.
  • ranked_pairs is the upper-triangle of the matrix sorted ascending by correlation — the most-uncorrelated pairs come first.
  • cells_detail is null unless include_cell_detail: true. Each entry's reason is "ok", "insufficient_overlap" (fewer than 3 paired observations), or "zero_variance" (constant series).
  • 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 in matrix (and reflected in cells_detail with the appropriate reason).

POST /kalshi/markets/edge

Per-ticker Octagon model-edge lookup. Accepts a mix of market and event tickers — markets are resolved to their parent event server-side. Pure DB read; the model is never invoked at request time.

Typical use: pull model priors for a known list of tickers, then feed model_probability into POST /kalshi/baskets/size for Kelly sizing.

Request body

FieldTypeRequiredDescription
tickersarray of stringsYes1–100 tickers. Mix of market_ticker and event_ticker values is allowed.
run_idstring (UUID)NoSpecific events_history run. Defaults to the most recent run.

Example

sh
curl -X POST "https://api.octagonai.co/v1/prediction-markets/kalshi/markets/edge" \
  -H "Authorization: Bearer your-octagon-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tickers": ["KXBTCD-26DEC31-T100000", "KXIPOSPACEX-26JUN01"]
  }'

Example response

json
{
  "run_id": "8c0e1b9a-1f44-4d2a-9b7e-2d6e5f50a912",
  "captured_at": "2026-05-25T19:39:00Z",
  "data": [
    {
      "input_ticker": "KXBTCD-26DEC31-T100000",
      "market_ticker": "KXBTCD-26DEC31-T100000",
      "event_ticker": "KXBTCD-26DEC31",
      "title": "Bitcoin above $100k by end of Dec 2026",
      "series_category": "Crypto",
      "model_probability": 0.62,
      "market_probability": 0.55,
      "edge_pp": 7.0,
      "expected_return": 0.13,
      "confidence_score": 6.0,
      "total_volume": 12345.67,
      "total_open_interest": 890.0,
      "status": "scored",
      "captured_at": "2026-05-25T19:39:00Z"
    }
  ]
}
  • market_ticker is null when the input was an event ticker (no per-market resolution applies).
  • status is "scored" when the parent event was scored in the run, or "unscored" when it wasn't — model_probability / edge_pp / expected_return will be null in that case.

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
market_tickersarray of stringsExplicit candidate pool — 1–200 tickers. Duplicates dropped, caller order preserved, capped to candidate_pool_size. Bypasses the search step entirely.
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"]).

Universe resolution order

  1. market_tickers supplied → fetch those tickers directly; skip the search step. The structured filters (category, series_ticker, min_volume_24h, close_before) still apply as post-filters so a noisy list silently narrows rather than failing later in the pipeline.
  2. q or anchor_ticker supplied → semantic similarity via find_similar_markets.
  3. Neither → structured-only via search_active_markets.

If both market_tickers and q / anchor_ticker are supplied, market_tickers wins. Tickers that don't exist in kalshi_markets_active or that fail the post-filters are silently dropped (no 4xx). Inspect universe_size in the response to confirm how many candidates survived — useful for detecting "all my tickers got filtered out" without comparing array lengths. label_contains_any still post-filters by thematic cluster label, max_per_cluster and max_pairwise_correlation still drive greedy selection, and the response shape is unchanged.

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
  }
}

POST /kalshi/baskets/validate

One-call portfolio diagnostics on a hand-built basket. Returns concentration metrics, side-aware pairwise correlations, calendar clashes, duplicate underliers, and a list of human-readable warnings — useful as a final sanity check before placing orders.

Request body

FieldTypeRequiredDescription
legsarray of objectsYesEach leg: { "market_ticker": string, "side": "yes" | "no", "stake_usd": number }.
bankroll_usdnumberNoWhen set, max_leg_pct is computed against bankroll_usd instead of total stake.
correlation_window_daysintegerYesLookback for the pairwise correlation matrix. Min 7; max 730.
correlation_intervalstringNo"1h" or "1d". Auto-picks 1d when correlation_window_days ≥ 90, else 1h.
max_pairwise_correlationnumberNoSoft threshold — emits a warning if any pair exceeds it. No legs are dropped.
calendar_clash_window_daysintegerYesWindow for grouping near-simultaneous close_time clusters. Min 1; max 90.

Side-aware: corr(YES_A, NO_B) = -corr(YES_A, YES_B), so YES/NO legs are flipped server-side without re-pulling candles.

Example

sh
curl -X POST "https://api.octagonai.co/v1/prediction-markets/kalshi/baskets/validate" \
  -H "Authorization: Bearer your-octagon-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "legs": [
      {"market_ticker": "KXGPT-OPEN-26JUL01",     "side": "yes", "stake_usd": 170},
      {"market_ticker": "KXAGANNOUNCE-26-SEP01",  "side": "no",  "stake_usd": 160}
    ],
    "bankroll_usd": 1000,
    "correlation_window_days": 30,
    "correlation_interval": "1h",
    "max_pairwise_correlation": 0.5,
    "calendar_clash_window_days": 7
  }'

Example response

json
{
  "total_stake_usd": 330.0,
  "bankroll_usd": 1000,
  "max_leg_pct": 0.17,
  "cluster_breakdown_thematic":  { "40": ["KXGPT-OPEN-26JUL01"] },
  "cluster_breakdown_behavioral": { "21": ["KXAGANNOUNCE-26-SEP01"] },
  "unassigned_market_tickers": [],
  "max_pairwise_correlation": -0.42,
  "pairwise_correlations": [
    { "ticker_a": "KXGPT-OPEN-26JUL01", "ticker_b": "KXAGANNOUNCE-26-SEP01", "correlation": -0.42 }
  ],
  "calendar_clashes": [
    { "window_start": "2026-07-01T00:00:00Z", "window_end": "2026-07-08T00:00:00Z", "market_tickers": ["KXGPT-OPEN-26JUL01"] }
  ],
  "duplicate_underliers": [
    { "event_ticker": "KXIPO", "market_tickers": ["KX-A", "KX-B"] }
  ],
  "warnings": [
    "Single leg is 45% of bankroll",
    "Pair KX-A / KX-B exceeds max_pairwise_correlation threshold (0.62 > 0.5)"
  ]
}
  • max_leg_pct is the largest leg's share of the denominator (bankroll_usd when supplied, otherwise the total stake).
  • cluster_breakdown_thematic / cluster_breakdown_behavioral map cluster IDs to the leg tickers that fall into them under the current runs. Legs without a current-run assignment land in unassigned_market_tickers.
  • pairwise_correlations already reflects side flips. max_pairwise_correlation is the largest absolute-value pair in the matrix.
  • calendar_clashes groups legs whose close_time falls inside a sliding window of size calendar_clash_window_days — surfaces "all my legs resolve on the same Fed meeting" risk.
  • duplicate_underliers lists events where the basket holds more than one child market (e.g. two SpaceX-IPO strikes).
  • warnings is a list of human-readable strings. The endpoint never rejects a basket — it surfaces issues for the caller to act on.

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.

14. Browse a series tree — every Bitcoin series sorted by volume

sh
curl -G "$BASE/v1/prediction-markets/kalshi/series" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "series_prefix=KXBTC" \
  --data-urlencode "sort_by=total_volume_24h"

15. Walk an event tree — every SpaceX-IPO strike

sh
curl "$BASE/v1/prediction-markets/kalshi/events/KXIPOSPACEX/markets?limit=50" \
  -H "Authorization: Bearer $OCTAGON_API_KEY"

16. Pull model priors for known tickers, then Kelly-size

sh
# Step 1: get model probabilities for the tickers you care about
curl -X POST "$BASE/v1/prediction-markets/kalshi/markets/edge" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tickers":["KXBTCD-26DEC31-T100000","KXETHU-26DEC31-T10000"]}'

# Step 2: feed those model_probability values into Kelly sizing
curl -X POST "$BASE/v1/prediction-markets/kalshi/baskets/size" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bankroll_usd": 1000,
    "kelly_multiplier": 0.25,
    "legs": [
      {"market_ticker":"KXBTCD-26DEC31-T100000","side":"yes","model_probability":0.62},
      {"market_ticker":"KXETHU-26DEC31-T10000","side":"yes","model_probability":0.58}
    ]
  }'

17. Sanity-check a hand-built basket before placing orders

sh
curl -X POST "$BASE/v1/prediction-markets/kalshi/baskets/validate" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "legs": [
      {"market_ticker":"KXGPT-OPEN-26JUL01","side":"yes","stake_usd":170},
      {"market_ticker":"KXAGANNOUNCE-26-SEP01","side":"no","stake_usd":160}
    ],
    "bankroll_usd": 1000,
    "correlation_window_days": 30,
    "max_pairwise_correlation": 0.5,
    "calendar_clash_window_days": 7
  }'

Inspect warnings, max_leg_pct, calendar_clashes, and duplicate_underliers before sending orders.

18. Top-N by volume across the entire universe — no client reranking

sh
curl -G "$BASE/v1/prediction-markets/kalshi/markets" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  --data-urlencode "sort_by=volume_24h" \
  --data-urlencode "limit=20"

19. Side-aware correlations — mix YES and NO legs in one call

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":["KX-A","KX-B","KX-C"],
    "sides":["yes","yes","no"],
    "window_days": 30,
    "include_cell_detail": true
  }'

matrix cells are already sign-flipped; use cells_detail[].reason to debug null cells.

20. Diversify a curated ticker list — CLI workflow

Use this when you already have a curated set of tickers (e.g. a basket build --theme macro command that resolves "macro" against its own registry) and want the server to handle correlation cap, per-cluster cap, and sizing over your list — not over a search result.

sh
curl -X POST "$BASE/v1/prediction-markets/kalshi/baskets/build" \
  -H "Authorization: Bearer $OCTAGON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "universe": {
      "market_tickers": [
        "KXRECSSNBER-26", "KXCPIYOY-26MAY-T4.5", "KXFEDRATE-26JUL",
        "KXGDPYOY-26Q2", "KXJOBS-26JUN", "KXM2YOY-26JUN"
      ],
      "min_volume_24h": 1000
    },
    "n": 4,
    "max_per_cluster": 2,
    "max_pairwise_correlation": 0.5,
    "candidate_pool_size": 50,
    "correlation_window_days": 30,
    "sizing": {"strategy": "equal"}
  }'

Tickers that don't match kalshi_markets_active (or that fail the post-filters) are silently dropped — check universe_size to confirm how many made it through. Bump candidate_pool_size if you're sending a registry larger than 50.


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.