Kalshi Bot: Read the Kalshi API and Track the Twitter (X) Post Markets It Settles

By TwitterAPI.io Team•15 min read
Diagram of a Kalshi bot pipeline: the Kalshi window, collecting posts with advanced search, counting posts, quotes and reposts without replies, and comparing with Kalshi brackets
The four steps of an X-post market tracker, and the result of our recount of 18 settled weeks (collected in 2026).

Most "Kalshi bot" guides stop at the order endpoint: sign a request, post an order, watch the fill. The harder part is the input. A bot is only as good as the data it reads before it trades, and for one family of Kalshi markets that data is public and countable: markets whose settlement is literally the number of posts an X account publishes in a week. This page is the developer map for that case: the Kalshi API surface (auth, demo, rate limits, public endpoints), the exact counting rule from Kalshi's contract terms, and a tracker that rebuilds the count from X data.

We did not take the rule on faith. For three organization accounts with weekly Kalshi markets (@federalreserve, @IMFNews and @wef), we recounted six settled weeks each from X data and compared our numbers with the brackets Kalshi settled and, where published, with Kalshi's exact count. The results, the misses and the edge cases are below, with the code.

One boundary before the code: Kalshi is a regulated exchange and these are real-money contracts. Nothing here predicts outcomes or promises a return. The tracker reads and reports; if you go further, paper-trade in Kalshi's demo environment first and read the risk section.

01 — Section

Which Kalshi markets actually settle on X posts

Kalshi's public API lists every series with its settlement sources. Filtering the series list for X as a source turns up a group whose underlying is an X post count. The contract terms behind them (the TWEETS contract, assets.kalshi.com/contract_terms/ECONTWEETS.pdf) are explicit: the underlying is "the number of posts on X by <account> in <period>", only main feed posts, quote posts and reposts count, the count is "determined by using the X API", and deleted posts are included if they were up long enough to be picked up by Kalshi's poller, "which is called every five minutes". The Source Agency is X.

SeriesX accountCadenceBrackets seen
KXFEDTWEETS@federalreserveweekly<9, 9-11, 12-14, 15-17, 18-20, >20
KXIMFTWEETS@IMFNewsweekly<11, 11-15, 16-20, 21-25, 26-35, >35
KXWEFTWEETS@wefweekly<15, 15-24, 25-34, 35-49, 50-74, >74
KXPOTUSTWEETS@realDonaldTrumpmonthlyabove 0 (did the account post at all)

Each weekly market's rules spell out the window: posts, quote posts and reposts "(not including replies)" from a Thursday at 12:00 PM ET through the next Thursday at 12:00 PM ET. Other series name X accounts too (an Elon Musk tweet series and a Pope tweets series exist in the series list) but had no listed markets when we checked, so build against what the markets endpoint returns today rather than a list in a blog post.

What about mention markets? Kalshi's Mentions category (what a person will say at a briefing, an earnings call or a match) mostly lists named media outlets and investor-relations event pages as settlement sources, not X. A bot can still use X as an early signal for those, but the settlement source is elsewhere, so this page focuses on the markets whose source is X itself.

Listings come and go. When we collected the data, the three weekly series had six settled weeks each on the public API and no open week. The tracker below checks for open markets first and says so when there are none.

02 — Section

The Kalshi API map: base URLs, auth, demo environment and rate limits

Base URLs. Production REST is https://external-api.kalshi.com/trade-api/v2 (Kalshi's market-data quick start). The demo REST API is https://external-api.demo.kalshi.co/trade-api/v2, with https://demo-api.kalshi.co/trade-api/v2 also supported, and a WebSocket at wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2. The demo web app lives at demo.kalshi.co.

Public endpoints need no key. GET /series/{series_ticker}, GET /markets (filter with series_ticker and status=open), GET /events/{event_ticker} and GET /markets/{ticker}/orderbook work without authentication. Prices arrive as dollar strings such as yes_bid_dollars: "0.6200", and counts with an _fp suffix such as volume_fp. Bracket bounds are floor_strike and cap_strike with a strike_type of less, between or greater.

Signed requests for everything private. Create a key in the API Keys section of your Kalshi profile; the private key is shown once and Kalshi does not store it. Each private request carries three headers: KALSHI-ACCESS-KEY (the key ID), KALSHI-ACCESS-TIMESTAMP (milliseconds) and KALSHI-ACCESS-SIGNATURE, the base64 signature of timestamp + HTTP method + path. Sign the path without its query string. Kalshi's docs make Ed25519 the default key type; RSA keys sign with RSA-PSS over SHA-256 with MGF1 SHA-256 and a digest-length salt. The official Python and TypeScript SDKs support both key types from version 3.31.0.

Demo is a separate world. The demo environment uses mock funds, and per Kalshi's docs "credentials are not shared between this environment and production", so generate a demo key on the demo site.

Rate limits. Kalshi meters requests in tokens per second, with separate read and write buckets. Most requests cost 10 tokens, so the sustained request rate is the budget divided by 10.

TierRead tokens/sWrite tokens/sHow you get it
Basic200100complete account signup
Advanced300300call the usage-level upgrade endpoint
Expert to Prime600 to 4,800600 to 4,800trading volume over a trailing 30 days, or assigned
Prestige12,0009,600trading volume, or assigned

The write bucket covers order placement, amends, cancels, order groups, the RFQ quote flow and block-trade accepts; every other call, including all GETs, draws from the read bucket, and GET /portfolio/balance has its own bucket. REST and FIX share the same buckets. For an X-post tracker the Kalshi side is light: one GET /markets per series per check is a few requests a minute at most.

The X side has its own limits. On TwitterAPI.io the read rate is 0.2 requests per second before the first payment, 20 once paid, 50 on Scale and 500 on Business. A tracker that checks three accounts every five minutes needs well under one request per second, so the free-tier rate is enough to prototype.

The snippet below signs a balance request against the demo environment.

python
# Signed request to the Kalshi DEMO environment (mock funds). RSA key shown;
# Kalshi's docs make Ed25519 the default key type and support both.
import base64, os, time
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

DEMO = "https://external-api.demo.kalshi.co"
KEY_ID = os.environ["KALSHI_DEMO_KEY_ID"]
with open(os.environ["KALSHI_DEMO_KEY_PATH"], "rb") as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)


def kalshi_headers(method, path):
    ts = str(int(time.time() * 1000))                 # milliseconds
    message = (ts + method + path.split("?")[0]).encode()  # sign path without query
    sig = private_key.sign(
        message,
        padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
                    salt_length=padding.PSS.DIGEST_LENGTH),
        hashes.SHA256(),
    )
    return {"KALSHI-ACCESS-KEY": KEY_ID,
            "KALSHI-ACCESS-TIMESTAMP": ts,
            "KALSHI-ACCESS-SIGNATURE": base64.b64encode(sig).decode()}


PREFIX = "/trade-api/v2"
path = PREFIX + "/portfolio/balance"
r = requests.get(DEMO + path, headers=kalshi_headers("GET", path), timeout=30)
print(r.status_code, r.json())
03 — Section

Counting X posts the way the contract does

The contract counts posts, quote posts and reposts, and excludes replies. In X data terms that means: keep every item from the account, drop items where isReply is true, and keep reposts (they carry a retweeted_tweet object) and quotes (a quoted_tweet object). On TwitterAPI.io the cleanest query is one advanced search per window: from:federalreserve include:nativeretweets since_time:<start> until_time:<end>, with queryType=Latest. The include:nativeretweets operator makes the search return the account's reposts as well, and reposts were 28.4% of @IMFNews's counted posts in our sample, so leaving them out would undercount badly.

Check the endpoint against the timeline first. Before the recount we compared the search with /twitter/user/last_tweets for @IMFNews over a shared 91-hour span: both returned the same 60 non-reply items, and the search additionally returned 7 replies, which the filter drops. That agreement is why the tracker uses one search per window instead of paging a timeline back several weeks.

The window is in Eastern Time. 12:00 PM ET is 16:00 UTC during daylight saving time and 17:00 UTC after it ends in November. Hard-coding 16:00 UTC is the most likely way to miscount a week; compute the window with zoneinfo and America/New_York, as the code does, and re-read each market's rules_primary, which states the window in words.

Deleted posts are the blind spot. Kalshi includes deleted posts its five-minute poller saw. A bot that counts only at the end of the week cannot see a post that was deleted on day two. If you want to match settlement as closely as possible, poll at least as often as Kalshi does and keep every id you have ever seen for the window.

04 — Section

Study: we recounted 18 settled Kalshi weeks from X data

For each of the three weekly series we took the six settled weeks available on Kalshi's public API, recounted posts + quotes + reposts (no replies) for the same Thursday-to-Thursday window from X data, and compared with the bracket that settled Yes. Twelve of the 18 events also publish Kalshi's exact count in the market's expiration_value field, which allows an exact comparison.

Bracket match: 18 of 18. Our count landed inside the settled bracket every week. Exact match: 9 of 12. Where Kalshi published the number, our count equaled it nine times. The three misses: @IMFNews week 1 (we counted 15, Kalshi 11, and our 15 included 4 reposts), @IMFNews week 6 (we counted 22, Kalshi 23) and @wef week 3 (we counted 8, Kalshi 10). From public data we cannot tell which side is wrong in those weeks; deleted posts that Kalshi's poller saw and later-removed reposts are both plausible. For a bot the lesson is that the settled number is Kalshi's, and two-post differences happen.

Account / weekSettled bracketOur countKalshi exact
@federalreserve wk 19-111010
@federalreserve wk 29-1199
@federalreserve wk 39-111010
@federalreserve wk 49-1110bracket only
@federalreserve wk 5<98bracket only
@federalreserve wk 618-201818
@IMFNews wk 111-151511
@IMFNews wk 2<1188
@IMFNews wk 3<111010
@IMFNews wk 426-3528bracket only
@IMFNews wk 526-3526bracket only
@IMFNews wk 621-252223
@wef wk 115-241717
@wef wk 215-241717
@wef wk 3<15810
@wef wk 4<158bracket only
@wef wk 5<1514bracket only
@wef wk 615-242222

Edges are where bots get hurt. In 7 of the 18 weeks the final count sat on a bracket edge, where one post more or less would have changed the settled bracket. Treat any count within one or two posts of an edge as unresolved until the window closes.

Three panels for @federalreserve, @IMFNews and @wef showing six weeks each: the settled Kalshi bracket as a bar and our recounted post count as a dot, all 18 dots inside their bars
Our recount against the settled bracket for 18 Kalshi weeks; hollow rings show the count if replies were included (collected in 2026).
05 — Section

The reply rule decides weeks, and so does timing

Replies are easy to forget because organization accounts rarely reply to others. They do reply to themselves to build threads, and those self-replies are replies. Across the 18 weeks we excluded 9 replies; @federalreserve accounted for 7 of them. Counting them would have moved 2 weeks into the wrong bracket: @federalreserve week 3 would have read 13 (12-14) instead of 10 (9-11), and week 5 would have read 9 (9-11) instead of 8 (<9). In both weeks Kalshi's settlement agreed with the reply-free count.

Half the week happens after Monday. The second chart shows, for each account, the median share of a week's final count already posted at the end of each day of the window. At Monday 12:00 PM ET, four days into the seven-day window, the median across all 18 weeks was 43%: @federalreserve 50%, @wef 41%, @IMFNews 38%. The weekday split explains it: @federalreserve and @wef posted almost nothing on Saturdays and Sundays (one weekend post from @federalreserve and none from @wef across six weekends, by UTC day), so the window's first Friday-to-Sunday stretch carries little information about the final count.

Weekly level shifts are large. @IMFNews's weekly count ranged from 8 to 28 (median 18.5), @wef's from 8 to 22 (median 15.5), and @federalreserve's from 8 to 18 (median 10). A bot that projects the final count from a few days of pace should expect to be wrong when an account's calendar changes, such as a policy meeting or a conference week.

Line chart of the median share of a week's counted posts already published by each day of the Kalshi window for three accounts, about 38 to 50 percent at Monday noon ET
Median share of the weekly count already posted by day of the window, six weeks per account (collected in 2026).
06 — Section

Build the tracker: count the current window and compare with open brackets

The full script at the end of this page does four things: compute the current Thursday-to-Thursday window in Eastern Time, run one advanced search per account for that window, count posts + quotes + reposts with replies dropped, and fetch the open Kalshi markets for the series to show which bracket the count is in now. It is read-only and places no orders.

Run it on a schedule. A five-minute schedule matches Kalshi's own poller interval and costs little: a check that finds nothing bills the 15-credit minimum ($0.00015), so polling every five minutes for a month is about $1.30 per query, and a combined query such as (from:federalreserve OR from:IMFNews OR from:wef) covers all three accounts at once. Keep a set of ids per window so a post deleted later still counts in your local tally, as it would at Kalshi if the poller saw it.

Add the two numbers a decision needs. The distance to the nearest bracket edge, and a projection of the final count. The helper below is deliberately simple: scale the current count by the share of a typical week that is usually done at this hour, using your own history for that account. Our study puts that share at roughly 38% to 50% at Monday noon ET for these accounts, with wide week-to-week variation, so the projection is a rough guide, not a forecast.

python
def projected_final(count_now, hours_elapsed, share_by_hour):
    """Naive pace projection: scale today's count by how much of a typical week
    is usually done at this hour. share_by_hour comes from YOUR OWN history
    of past windows for the same account (e.g. 0.43 at Monday noon ET)."""
    share = max(share_by_hour(hours_elapsed), 0.05)
    return count_now / share


def edge_distance(n, m):
    """How many more posts until the count leaves bracket m (inclusive bounds)."""
    lo, hi = m.get("floor_strike"), m.get("cap_strike")
    if m["strike_type"] == "less":
        return hi - n                 # posts left before reaching the cap
    if m["strike_type"] == "greater":
        return None                   # open-ended upper bracket
    return hi - n + 1
07 — Section

Cost: Kalshi data is free, X data is pay-per-tweet

Kalshi's public market data costs nothing to read. The X side is billed. On TwitterAPI.io tweet endpoints cost 15 credits per tweet returned, which is $0.15 per 1,000 tweets, with a 15-credit minimum per request, so a check that returns nothing costs $0.00015 and a full 20-tweet page costs $0.003. No X account is needed for reads.

WhatTwitterAPI.ioOfficial X API (pay-per-usage)
Per 1,000 tweets read$0.15$5.00 ($0.005 per post read)
The 269 posts behind our 18-week recountabout $0.04$1.345
Polling floor, every 5 minutes for a month$1.30billed per post read

The ratio is about 33x per 1,000 tweets ($5 versus $0.15), using the official X API rate card at docs.x.com/x-api/getting-started/pricing, which also caps pay-per-usage at 3 million post reads a month. The whole study, including the pilot that validated the endpoint, took 58 requests and about $0.08. New accounts that pass the automatic bot check get up to $1 of free credits (at most 100,000 credits, no card, one per person or network, expiring after 31 days), which covers a prototype tracker for a long time; plans and rates are on the pricing page.

08 — Section

Rules, eligibility and risk before you trade

Regulation and eligibility, from Kalshi. Kalshi's help center states that "Kalshi is subject to U.S. regulatory oversight by the CFTC." To open an account you must be 18 or older and pass document verification if requested. Trading from outside the United States is possible from many countries, but eligibility is governed by the Kalshi Member Agreement, which lists the restricted jurisdictions; check it for your country before writing any trading code. These weekly series also prohibit trading by employees of the source agencies and by anyone with material non-public information on the underlying.

Contract details that matter to a bot. From the TWEETS contract terms: minimum tick $0.01, settlement value $1.00, a position accountability level of $25,000 per strike per member, and a rule that if there is credible reporting that the account was hacked, Kalshi may determine payouts under its rulebook. If an account is renamed during a live market, posts under the old and new names are summed.

Risk note. Event contracts can lose the full amount paid. Our recount shows the count is observable, not that any price is wrong: other traders see the same public posts, and the remaining uncertainty is about posts that have not happened yet. Nothing on this page is a trading recommendation or a claim of an edge. Start in the demo environment with mock funds, log every decision your bot would have made, and compare it with how markets settled before using real money.

Bot hygiene. Keep keys out of source control, put a hard cap on order size and daily loss in code, add a kill switch, handle 429 responses by backing off, and keep demo and production keys in separate configs, since Kalshi does not share credentials across the two.

09 — Section

How we collected this

Question. If a bot rebuilds Kalshi's X post count from public X data, does it land in the bracket Kalshi settled, and how much of the week's count is known by mid-week?

Accounts and markets. @federalreserve, @IMFNews and @wef (all organization accounts), matched to Kalshi series KXFEDTWEETS, KXIMFTWEETS and KXWEFTWEETS. Kalshi data came from the public GET /markets?series_ticker= endpoint (no authentication): bracket bounds, results, expiration_value and volume_fp (47,485 contracts traded across the 18 events).

Window and sample. The six settled weekly windows per series available at collection time, each Thursday 12:00 PM ET to the next Thursday, 18 account-weeks in total, collected in 2026. X data came from /twitter/tweet/advanced_search with from:<account> include:nativeretweets and Unix-time bounds per window: 269 items returned, 260 counted after dropping 9 replies, 40 requests, plus an 18-request pilot comparing the search with /twitter/user/last_tweets.

What the sample cannot show. Posts deleted before we collected are invisible to us, which may explain some exact-count differences. Three accounts and six weeks each are a small sample; other accounts, holiday weeks and the switch out of daylight saving time can behave differently. The study says nothing about prices, liquidity or whether any trade would have made money.

Privacy. Aggregates only, organization accounts only; we did not analyze the authors of reposted or quoted posts. The protocol, analysis script and results are in the repository under data/studies/kalshi-bot/.

python
"""Kalshi X-post market tracker: count posts in the current Kalshi window and
compare with the open brackets. Read-only: it places no orders.

pip install requests
export TWITTERAPI_IO_KEY=...   # from twitterapi.io/dashboard
"""
import os
import time
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo

import requests

BASE = "https://api.twitterapi.io"
KALSHI = "https://external-api.kalshi.com/trade-api/v2"   # public market data, no auth
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
ET = ZoneInfo("America/New_York")

SERIES = {"KXFEDTWEETS": "federalreserve", "KXIMFTWEETS": "IMFNews", "KXWEFTWEETS": "wef"}


def open_brackets(series):
    r = requests.get(f"{KALSHI}/markets",
                     params={"series_ticker": series, "status": "open"}, timeout=30)
    r.raise_for_status()
    return r.json().get("markets", [])


def bracket_contains(m, n):
    lo, hi, kind = m.get("floor_strike"), m.get("cap_strike"), m.get("strike_type")
    if kind == "less":
        return n < hi
    if kind == "greater":
        return n > lo
    return lo <= n <= hi                          # "between" is inclusive


def current_window(now=None):
    """Thursday 12:00 PM ET to the next Thursday 12:00 PM ET (check each market's rules)."""
    now = (now or datetime.now(timezone.utc)).astimezone(ET)
    days_back = (now.weekday() - 3) % 7          # Thursday = 3
    start = (now - timedelta(days=days_back)).replace(hour=12, minute=0, second=0, microsecond=0)
    if start > now:
        start -= timedelta(days=7)
    return start, start + timedelta(days=7)


def count_posts(account, start, end):
    """Posts + quote posts + reposts, replies excluded, deduplicated by id."""
    query = (f"from:{account} include:nativeretweets "
             f"since_time:{int(start.timestamp())} until_time:{int(end.timestamp())}")
    seen, cursor = {}, ""
    while True:
        params = {"query": query, "queryType": "Latest"}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(f"{BASE}/twitter/tweet/advanced_search",
                         headers=HEADERS, params=params, timeout=30)
        if r.status_code == 429:
            time.sleep(5)
            continue
        r.raise_for_status()
        data = r.json()
        for t in data.get("tweets", []):
            if not t.get("isReply"):
                seen[t["id"]] = t
        if not data.get("has_next_page") or not data.get("next_cursor"):
            break
        cursor = data["next_cursor"]
    kinds = {"repost": 0, "quote": 0, "post": 0}
    for t in seen.values():
        kinds["repost" if t.get("retweeted_tweet") else "quote" if t.get("quoted_tweet") else "post"] += 1
    return len(seen), kinds


if __name__ == "__main__":
    start, end = current_window()
    print(f"window {start:%a %H:%M} ET -> {end:%a %H:%M} ET")
    for series, account in SERIES.items():
        n, kinds = count_posts(account, start, end)
        print(f"\n@{account}: {n} counted so far {kinds}")
        markets = open_brackets(series)
        if not markets:
            print("  no open Kalshi market for this series right now")
        for m in sorted(markets, key=lambda m: m.get("floor_strike") or -1):
            here = "  <- count is here now" if bracket_contains(m, n) else ""
            print(f"  {m['yes_sub_title']:>7}  yes bid {m.get('yes_bid_dollars')}  "
                  f"ask {m.get('yes_ask_dollars')}{here}")
10 — Questions

Questions readers ask

Does Kalshi have an API for bots?

Yes. The Kalshi Trade API serves market data and trading. Public market data (series, events, markets, order books) needs no key at external-api.kalshi.com/trade-api/v2; private endpoints such as orders and balances need an API key and three signed KALSHI-ACCESS-* headers. Kalshi also publishes official Python and TypeScript SDKs.

How do I authenticate to the Kalshi API in Python?

Create an API key in your Kalshi profile and keep the private key (it is shown once). For each private request, sign the string timestamp-in-milliseconds + HTTP method + path (without the query string) and send KALSHI-ACCESS-KEY, KALSHI-ACCESS-TIMESTAMP and KALSHI-ACCESS-SIGNATURE. Ed25519 is the default key type; RSA keys use RSA-PSS with SHA-256. The code on this page shows the RSA version.

Is there a Kalshi demo or paper-trading environment?

Yes. The demo web app is at demo.kalshi.co and the demo REST API at external-api.demo.kalshi.co/trade-api/v2. It uses mock funds, and credentials are not shared with production, so create separate demo keys.

What are the Kalshi API rate limits?

Tiers start at Basic with 200 read and 100 write tokens per second; most requests cost 10 tokens. Advanced (300/300) is available through an upgrade endpoint, and higher tiers up to Prestige (12,000 read, 9,600 write) come from trading volume over a trailing 30 days or assignment by Kalshi.

Which Kalshi markets settle on posts on X?

Kalshi's TWEETS contract settles on the number of posts on X by one account in a period, with X as the Source Agency. Weekly series for @federalreserve, @IMFNews and @wef count posts, quote posts and reposts, excluding replies, from Thursday 12:00 PM ET to the next Thursday; a monthly series asks whether @realDonaldTrump posts at all. Check the markets endpoint for what is listed now.

Are Kalshi mention markets based on tweets?

Mostly not. Mention markets ask what a person will say at an event, and the ones we checked list transcripts, media outlets or investor-relations pages as settlement sources. X can be an early signal for them, but the post-count series are the markets that settle on X itself.

Can a bot reproduce Kalshi's post count exactly?

Closely, not perfectly. In our study the recount landed in the settled bracket in 18 of 18 weeks and matched Kalshi's exact count in 9 of the 12 weeks where it was published. Deleted posts that Kalshi's five-minute poller saw are the main thing a later count cannot see.

Is running a Kalshi trading bot profitable?

Nothing guarantees it. Event contracts can lose the full amount paid, other traders see the same public posts, and our study measures counting accuracy, not trading results. Check eligibility in Kalshi's Member Agreement, paper-trade in the demo environment, and cap risk in code before using real money.

11 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key