DEV Community

Nikita Iakovlev
Nikita Iakovlev

Posted on

Kalshi's Market Titled 'HIGHEST Temperature SATX' Settles in Louisville

Visas are my day job — I run an agency in Bali. Scrapers are the other half of the week, and one of them reads Kalshi's daily temperature markets: the ladders of brackets where a contract pays $1 if tomorrow's high in a given city lands in a given two-degree band. The prices are a crowd-sourced probability distribution over the weather, which is a genuinely interesting dataset and free to read — Kalshi's market API needs no key and no account.

I thought the hard part would be the bracket arithmetic. It wasn't. The hard part was answering the question which city is this market about, and the reason is that Kalshi answers it three times, in three places, and they do not always agree.

Three location fields, two of them unreliable

Here are three live calls against the public API, made today, 12 October 2026. First the series list:

curl -s 'https://api.elections.kalshi.com/trade-api/v2/series?category=Climate%20and%20Weather'
Enter fullscreen mode Exit fullscreen mode

Two of the 422 entries it returns:

KXHIGHTSDF   | HIGHEST Temperature SATX
KXHIGHTSATX  | San Antonio Daily Maximum Temperature
Enter fullscreen mode Exit fullscreen mode

The series whose ticker ends in SDF — the IATA code for Louisville — is titled with SATX, San Antonio. Now the event under that same series:

curl -s 'https://api.elections.kalshi.com/trade-api/v2/events?series_ticker=KXHIGHTSDF&with_nested_markets=true&status=open&limit=1'
Enter fullscreen mode Exit fullscreen mode
event  KXHIGHTSDF-26OCT12 | Highest temperature in Louisville on Oct 12, 2026?
rules: If the maximum temperature recorded at Louisville (CLISDF) for Oct 12, 2026,
       is less than 82° fahrenheit according to The Weather Company, then the market
       resolves to Yes.
Enter fullscreen mode Exit fullscreen mode

Series title says San Antonio. Event title says Louisville. Settlement rules say Louisville, station CLISDF. Its sibling KXLOWTSDF is titled "Lowest Temperature SDX", which is not a code for anywhere.

So: the only field that is load-bearing is the one inside the resolution rules, because that is the string the exchange pays out against. Everything above it is a label a human typed. My parser is one line, and it reads the rules:

export function stationFromRules(rules) {
    const m = String(rules || '').match(/\(([A-Z]{3}[A-Z0-9]{2,4})\)/);
    return m ? m[1] : null;
}
Enter fullscreen mode Exit fullscreen mode

The city then comes out of a station table, never out of the title. The title is a fallback label and nothing else.

Why the station, not the city, decides your forecast

If you only want prices this is trivia. The moment you want to put a weather forecast next to the price, it becomes the whole problem — because these markets do not settle on "the weather in Chicago". They settle on one climate report from one point on the map:

Market Settles at Not at
Chicago CLIMDW — Midway, southwest side O'Hare
Los Angeles CLILAX — the airport, by the ocean downtown
Houston CLIHOU — Hobby Intercontinental (CLIIAH)
New York City CLINYC — Central Park Newark or LaGuardia

Newark and Trenton are their own markets, by the way — KXHIGHTEWR and KXHIGHTTTN. "New York weather" is three separate ladders.

I measured what guessing costs. Same forecast provider (Open-Meteo, also keyless), two coordinate pairs each, daily high for the next three days, pulled this afternoon:

City Settlement station The obvious alternative Biggest gap in 3 days
Los Angeles LAX 77.6° (14 Oct) downtown 81.8° 4.2°
San Francisco SFO 72.5° (13 Oct) downtown 69.1° 3.4°
Houston Hobby 93.4° (13 Oct) Intercontinental 90.3° 3.1°
Chicago Midway 66.2° (14 Oct) the Loop 63.3° 2.9°

Phoenix and Washington stayed inside half a degree, so this is not a uniform correction you can shrug off — it is coastal and lake-effect geography, and it is biggest exactly where the volume is. Kalshi's brackets are two degrees wide. A 4.2° error is two whole brackets. You would be comparing the market's price for "77° to 78°" against a forecast that belongs in "81° or above" and calling it an edge.

Observations have the same trap, and a nice shortcut out of it: Kalshi's climate-report code maps onto the NWS station id by swapping the CLI prefix for K. CLIMDW → KMDW, and https://api.weather.gov/stations/KMDW/observations gives you what the settling station has actually recorded, free. (Settlement itself is "according to The Weather Company" per the rules, so treat NWS obs as a sanity check on the same thermometer, not as the official number.)

The field that tells you the forecast is stale, not smart

That observation column turned out to be the most useful thing in the output. A real row from a run I made an hour ago, Chicago high for today:

{
  "city": "Chicago", "station": "CLIMDW", "date": "2026-10-12",
  "favourite": "88° to 89°", "favouriteProbability": 99,
  "marketExpected": 88.4,
  "forecastValue": 86.2,
  "forecastMinusMarket": -2.2,
  "forecastBracket": "86° to 87°",
  "forecastBracketProbability": 1,
  "observedHigh": 88, "observedLow": 62.6,
  "volume": 100723.06
}
Enter fullscreen mode Exit fullscreen mode

Read the first five fields and you have found a disagreement: the model says 86.2, the market is 99% certain of 88–89, and the bracket the model points at is on sale for one cent. Read observedHigh and the story collapses — Midway had already recorded 88° when I pulled this. The high was in. The market wasn't confident, it was informed; the model run was old.

Any forecast-versus-market row for a day already under way needs that column, or you will mistake lag for alpha. For tomorrow, where there is nothing recorded yet, the same actor's Chicago row shows a real gap: market expects 82.2, model says 77.0, and the bracket the model picks is the favourite at 52% — a disagreement about magnitude inside a band everyone agrees on.

One more honesty flag in that example. Tomorrow's favourite is "81° or below", an open-ended end bracket with no true midpoint. When most of the money sits in one of those, a weighted average of midpoints is capped by construction and understates the market. That row comes back with marketExpectedIsBounded: true so you know not to quote the number as a temperature. Use forecastBracketFairProbability instead — what the market charges for the bracket your model actually picked.

Days nobody traded

Kalshi opens an event per city per day whether or not anyone shows up. In yesterday's full snapshot — 48 open events, 288 brackets, 24 cities, highs and lows — 9 of the 48 ladders priced to less than 0.85 in total:

Chicago    low   sum 0.46
San Diego  low   sum 0.58
Seattle    low   sum 0.64
Minneapolis low  sum 0.69
...
Enter fullscreen mode Exit fullscreen mode

A healthy six-bracket ladder sums to a little over 1.00 — the surplus is the exchange's cut. These sum to half of 1.00, because every bracket is sitting at a penny of stale quote on a few dollars of volume. Scale those six pennies to 100% and you have manufactured a confident-looking distribution out of nothing; one of those rows reports an "overround" of −0.31, which is not a tight market, it is an empty one. So they come back flagged illiquid: true, fair probabilities withheld rather than invented, and the forecast mode drops them entirely. Overnight lows are where this clusters — nobody trades them, and that is the data, not a bug.

Calling it

Input is four fields; this is the comparison mode for two cities, one day ahead:

curl -X POST "https://api.apify.com/v2/acts/lergassy~kalshi-weather-scraper/run-sync-get-dataset-items?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"mode":"forecast","metrics":["high"],"cities":["Louisville","Chicago"],"days":1,"withForecast":true}'
Enter fullscreen mode Exit fullscreen mode

Four rows, 33 seconds, and the Louisville rows are correctly labelled Louisville no matter what the series title says. It's on the Apify Store as Kalshi Weather Markets — brackets, days, forecast-versus-market and the raw series list, pay per row.

When this is the wrong tool

  • You want to trade from it. It places no orders and holds no positions. It reads public prices and public weather.
  • You want weather outside the US. The series list includes Hong Kong and Berlin temperature series; my station table covers the 24 US cities that settle on a CLI climate report, and anything else comes back without coordinates.
  • You want rain, snow or hurricanes. Those series exist in quantity — 30 rain, 23 hurricane, 9 snow in today's list — and usually have no open event. Of the 104 daily high/low series, only 48 had a live event yesterday. Daily highs and lows are what actually trades.
  • You want to run it from a laptop in the wrong country. Kalshi's API refuses connections from a number of regions, Indonesia among them. That is the honest reason this one lives on a cloud platform instead of my machine.

The portable lesson has nothing to do with prediction markets: when a feed describes the same thing in several fields, find out which field the money is settled against, and parse that one. Here it was a sentence of English prose, three layers down, contradicting the title at the top.

Top comments (1)

Collapse
 
botdevsupports profile image
DEV SUPPORT •
You need to verify your account.
Enter fullscreen mode Exit fullscreen mode

tr.ee/dev-to