Skip to main content

langbot-sdk

Python SDK and CLI for Langbot, the data and simulation layer for every market on Kalshi and Polymarket. Look up any market and its price history, replay a strategy against the order books Langbot recorded, with real fees and latency, then run the same file live with your own Kalshi keys: paper first, live only when you say so.

pip install langbot-sdk      # Python 3.12+; the import name and the command are `langbot`
uv tool install langbot-sdk  # or: just the `langbot` command, isolated

The PyPI name langbot belongs to an unrelated project; install langbot-sdk.

No token needed for the first step:

langbot markets senate                           # search every Kalshi and Polymarket market
langbot market KXEARNINGSMENTIONGS-26OCT13-PRIV  # one market: price history, result, replayable books
langbot events                                   # live and upcoming events with markets

With a free token (langbot login):

langbot sim events                               # events with recorded books
langbot sim run -e ev_kxearningsmentiongs_26oct13 -s my_strategy.py   # replay, scorecard
langbot run my_strategy.py --mode demo           # the same file, paper, on the live feed
langbot calendar --days 7                        # upcoming events, speakers, rights
langbot context KXFEDMENTION-26OCT-TARIFF        # rules + book + base rate + event + mentions
langbot rates --speaker powell --phrase transitory [--series KXFEDMENTION] [--window-days 365]
langbot outcomes --speaker powell --since 2026-01-01   # settled markets, paginated (--cursor)

Every command takes --json for the raw response.

Client

Runs as is, without a token (anonymous calls get the Free delay, +60 s):

import asyncio

from langbot import Langbot  # alias of LangbotClient


async def main() -> None:
    # token: LANGBOT_API_TOKEN, else the one `langbot login` saved, else anonymous
    # api_url: LANGBOT_API_URL, else https://api.langbot.ai (stream: wss://api.langbot.ai)
    async with Langbot() as lb:
        hits = await lb.recent_events(types=["phrase_detected"], limit=5)
        for e in hits:
            print(e.event_id, e.phrase, e.t_spoken, e.verdict.counts if e.verdict else None)
        if hits:
            page = await lb.transcript(hits[-1].event_id)  # words, text, last_seq
            print(page.text[:200] or f"words withheld for {page.redacted_sources}")
        async for evt in lb.stream(types=["phrase_detected"]):
            print(evt.phrase, evt.t_spoken, evt.lag_vs_public_ms)


asyncio.run(main())

transcript() returns one page (words, text, last_seq, held, delay_ms, redacted_sources); poll with since=page.last_seq. Restricted sources return no words.

Feed errors raise LangbotAPIError (.status, .detail, .url); LangbotApiError is the same class under the TypeScript SDK's spelling. A stream refused on policy (close 1008: your plan's connection cap) raises StreamRefused instead of reconnecting. A local feed: Langbot("http://localhost:8000"). Examples that run against the hosted feed: examples/ (recent_hits.py, transcript_pages.py, stream_hits.py).

Same calls in the TypeScript SDK

The TypeScript SDK uses the Python SDK's names in camelCase (recent_events → recentEvents). The old TypeScript names (getEvent, getTranscript, listMarkets, listSubscriptions, listTokens) still work for one release, marked deprecated.

Python (langbot) TypeScript (@langbotai/sdk)
client LangbotClient / Langbot LangbotClient / Langbot
live + upcoming events live_events() liveEvents()
one event event(id) event(id)
recent hits (one page) recent_events(event_id, phrases=, types=, since=, until=, cursor=, limit=) recentEvents({eventId, phrases, types, since, until, cursor, limit})
every page, newest first iter_recent_events(...) iterRecentEvents({...})
transcript page transcript(id, since=) → Transcript transcript(id, {since}) → Transcript
every market (no token) public_markets(q, venue=, status=, category=, limit=) publicMarkets({q, venue, status, category, limit})
one market + history (no token) public_market(ticker) publicMarket(ticker)
markets with recorded events markets(q=, venue=, status=) markets({q, venue, status})
subscriptions / tokens subscriptions(), tokens() subscriptions(), tokens()
live stream stream(...), stream_sse(...) stream(...), streamSSE(...)
feed error LangbotAPIError (.status, .code, .detail, .url) LangbotAPIError (.status, .code, .detail, .url, .body)
refused stream (1008) raises StreamRefused (.code) onStatus({state: "closed", code: 1008, willRetry: false}) after a server_error
token default LANGBOT_API_TOKEN, else ~/.config/langbot/config.toml LANGBOT_API_TOKEN (the npm CLI also reads config.toml)
config file (both CLIs) ~/.config/langbot/config.toml, LANGBOT_CONFIG the same file (falls back to the old config.json)

Both read unknown keys and unknown enum values without failing, and both are tested against the feed's own response shapes (packages/schemas/fixtures/feed-responses.json).

History, calendar and context

from langbot import LangbotClient

async with LangbotClient() as lb:
    # list[CalendarEntry]: .rights, .speaker_keys, .phrases, .restricted
    cal = await lb.calendar(days=7)
    # MarketContext: .market .book .base_rates .event .mentions .rules
    ctx = await lb.context("KXFEDMENTION-26OCT-TARIFF")
    # BaseRates: .yes_rate (P(YES)), .markets (sample), .mispricing (avg result - open)
    br = await lb.base_rates(speaker="powell", phrase="transitory", window_days=365)
    # OutcomesPage: .items (list[MarketOutcome]), .next_cursor
    page = await lb.outcomes(speaker="powell", limit=50)
    # list[Mention]: phrase facts only, never transcript text
    facts = await lb.mentions(phrase="blackwell", since=1_760_000_000_000)
    who = await lb.speakers(q="pow")  # list[SpeakerProfile]

Base rates and outcomes are the venue's public settlement record; mentions are Langbot's own phrase_detected facts. For restricted / unknown sources only phrase facts are ever returned (no transcript text) — CalendarEntry.restricted tells you ahead of time.

Write a strategy

One protocol for langbot run, langbot backtest and langbot sim run: a class with a synchronous on_event(evt, ctx) (the sim protocol below; langbot.adapter drives it live).

class Mention:
    markets = ["KXEARNINGSMENTIONNVDA-*"]  # glob over tickers
    phrases = ["blackwell"]

    def on_event(self, evt, ctx):
        if evt["type"] == "phrase_detected" and evt.get("phrase") == "blackwell":
            for ticker in evt["market_refs"]:
                book = ctx.book(ticker)
                if book and book.yes_ask and book.yes_ask < 0.60:
                    ctx.buy(ticker, "yes", 10, limit=book.yes_ask, tif="ioc")

ctx: now, tickers, book(ticker), position(ticker), orders(ticker), buy(ticker, side, qty, limit=None, tif=None), sell(...), cancel_all(ticker=None). The older class X(langbot.Strategy) with async hooks still runs, with a deprecation warning.

Every real order passes through the live guard (langbot.live): hard limits from ~/.config/langbot/live.toml (per order, per market, total exposure, open orders, orders per minute, daily loss, price through the book, series allow/deny; a missing file means the smallest limits), a kill switch (langbot live halt / resume, plus automatic halts on daily loss, reconciliation mismatch, repeated API errors, stale books and clock skew), reconciliation against Kalshi every 15 s, idempotent client order ids, and an append-only audit log.

langbot live run my_strategy.py --dry-run            # Kalshi demo, nothing sent
langbot live run my_strategy.py                      # Kalshi demo
langbot live run my_strategy.py --env prod --yes-real-money
langbot live halt --cancel-orders                    # stop now, pull resting orders

Demo is the default. Prod needs --yes-real-money and a non-empty series_allow. See docs/risk.md for every limit and halt.

Langbot Sim: test a bot against a past event

langbot.sim replays a recorded event through your strategy against the Kalshi order books we recorded while it happened (1 s snapshots), then scores it. Your code runs on your machine; the feed serves the timeline (GET /v1/sim/events/{id}/timeline) and only ever runs its own built-in strategies (POST /v1/sim/runs). Both use this same engine, so the same strategy on the same event gives the same scorecard locally and on the server.

# my_bot.py
class FirstHit:
    name = "first_hit"

    def __init__(self, qty=10, max_price=0.90):
        self.qty, self.max_price = qty, max_price

    def on_event(self, evt, ctx):  # every feed event, as the stream sends it
        if evt["type"] != "phrase_detected":
            return
        if (evt.get("verdict") or {}).get("counts") is False:  # §21: does not settle
            return
        for ticker in evt["market_refs"]:
            if not ctx.position(ticker) and not ctx.orders(ticker):
                ctx.buy(ticker, "yes", self.qty, limit=self.max_price, tif="ioc")

    # optional: def on_book(self, ticker, book, ctx), on_fill(self, fill, ctx),
    #           on_start(self, ctx), on_end(self, ctx)
langbot sim events                                   # what can be replayed, with coverage
langbot sim run --event ev_kxearningsmentiongs_26oct13 \
    --strategy my_bot.py:FirstHit --latency 250,2000,5000 --params '{"qty": 5}'
langbot sim run --event ev_… --strategy my_bot.py --timeline gs.ndjson   # cache, then offline
langbot sim strategies                               # the built-ins
langbot sim run --event ev_… --strategy fade_rule_mismatch --server      # a built-in, on the feed
langbot research bench --strategy my_bot.py --latency 250,2250           # every recorded event, one document
langbot research calibration --speaker powell --basis mid                # openings vs settlements, with intervals

Hooks are synchronous (no await); the clock is ctx.now (ms). ctx.buy(ticker, side="yes"|"no", qty, limit=None, tif=None) / ctx.sell(...) return an Order; ctx.position(ticker) (.yes, .no, .net), ctx.book(ticker) (yes_bid, yes_ask, no_bid, no_ask, mid, bids, asks), ctx.orders(ticker), ctx.cancel(order_id), ctx.cash, ctx.tickers. Strategies never see settlement results.

Fill model (langbot/sim/engine.py has the details):

  • An order placed at ctx.now arrives at now + latency and matches the first snapshot at or after that, walking its depth levels; size that runs out leaves a partial fill. A market order (no limit) or tif="ioc" cancels the rest; a limit order rests.
  • A resting order fills only when a later snapshot's best opposite price moves through its limit (not on touch), at the limit, up to the size shown through it.
  • No market impact: the recording never reacts to you (size you take is gone only within that snapshot). --queue-position makes resting orders wait behind the size recorded at their price.
  • Fees per order execution: ceil(0.07 · Σ C·P·(1−P) · 100) / 100 taker; resting fills pay 0.0175.
  • Positions settle at the market's result (1.00 / 0.00); a market without one is marked to its last mid and flagged.

The scorecard: P&L (gross, fees, net), per market, orders and fills, fill rate, max drawdown, exposure, a latency table (by default your latency, +2 s, +5 s) and the caveats that apply.

In code:

from langbot import LangbotClient
from langbot.sim import load_strategy, run_scorecard

async with LangbotClient() as lb:
    timeline = await lb.sim_timeline("ev_kxearningsmentiongs_26oct13")
card = run_scorecard(timeline, load_strategy("my_bot.py:FirstHit"), latencies=[250, 2250, 5250])
print(card["pnl"], card["latency_sensitivity"])

For a timeline too big to hold, stream it: ScorecardRun(Timeline.header(event_id, markets, booked=tickers_with_books), strategy, latencies=[...]), then feed(page) for each page of TimelineItems in timeline order and finish(). Every latency runs in the same pass, memory stays at a book per ticker plus your orders and fills, and the scorecard is identical to run_scorecard on the whole timeline. The feed's server runs work this way.

Event study. How do prices move after the speaker says a word? langbot sim study pools every trigger (detected phrases, and transcript words when you pass --words; multi-word phrases match consecutive words) across events and reports, per (word, ticker), the mean / median / stdev move of the YES mid in cents and the share up / down / flat at each horizon, plus a chronological train/test split and caveats (small n, how many pairs you tested):

langbot sim study --event ev_a --event ev_b --words "tariff,rate cut" --horizons 5,30,300
langbot sim study --event ev_a --words tariff --tickers KX… --clock spoken --json

Triggers are timed by default when the data was emitted (--clock emitted, plus --delay-ms), i.e. when you could have traded. --clock spoken uses when the words were said and is not tradeable: the broadcast can run 20-30 s behind the market, so it credits words with moves that already happened. In code: event_study(timeline, words=[...], tickers=None) or event_study_many([tl_a, tl_b], ...) from langbot.sim.

Plans: every plan runs Sim on any recorded event. Free has 20 runs a month, Builder 100, Standard and up unlimited. A run is one server run or one timeline download (its first page). Past the allowance the feed answers 429 with code == "quota_exceeded"; the error's .quota says what was used and when it resets (resets_at).

Modules

module what
langbot.schemas pydantic models mirroring packages/schemas/event.schema.json (Event, Speaker, EventType, SourceKind) plus Book, Fill, Order, Market, LiveEvent, MarketOutcome, BaseRates, SpeakerProfile, Mention, CalendarEntry, MarketContext, new_event_id()
langbot.client Langbot / LangbotClient — every feed endpoint incl. calendar(), context(), base_rates(), outcomes(), mentions(), speakers(); stream() (WS, auto-reconnect), stream_sse()
langbot.kalshi KalshiClient — Trade API v2 with RSA-PSS / Ed25519 signing, token-bucket rate limiter, ws_subscribe() with orderbook reconstruction
langbot.strategy Strategy, RiskRails, PaperBroker, KalshiBroker, Position
langbot.live LiveGuard, GuardedBroker, Limits/load_limits, KillSwitch, AuditLog, the Venue protocol, KalshiVenue, PolymarketUSVenue
langbot.runtime langbot run: feed stream + Kalshi books + local daemon on 127.0.0.1:8787
langbot.backtest replay archived events + book snapshots (feed or JSONL) at original timestamps
langbot.sim Langbot Sim: the deterministic engine (fill model, fees, settlement; queue_position, taker_delay_ms, maker_fee_rate settings), scorecard and built-in strategies the feed also runs (quote_both_sides is the market-making baseline); event_study for price moves after words; langbot.sim.bench aggregates a strategy across events (bootstrap interval, p-values with FDR, fee sensitivity, walk-forward) and langbot.sim.stats holds the shared tests
langbot.notify Discord / Slack / Telegram notifiers (notifier_from_env())
langbot.cli the langbot command

Paying per call

Some endpoints are sold by the call (evidence packs, provenance reports, replay bundles, history, facts — CONTRACTS §17). Without a plan that covers them the feed answers 402 Payment Required in the x402 v2 shape, and the client can pay it in USDC with a wallet you hold:

pip install "langbot-sdk[x402]"        # adds the official x402 client + eth-account (pulls web3)
export LANGBOT_X402_WALLET=0x…         # hex private key of a funded wallet; yours, never Langbot's
import os
from langbot import LangbotClient, PaymentRequired

async with LangbotClient(x402_wallet=os.environ["LANGBOT_X402_WALLET"], x402_max_usd=1.0) as lb:
    bundle = await lb.replay("ev_nvda_q3_2026")  # 402 → signed retry → 200, one call
    print(lb.last_payment)  # {price_usd, header, network, settlement: {transaction, payer, …}}

What happens on a 402 whose PAYMENT-REQUIRED header and JSON body carry the x402 v2 PaymentRequired object {x402Version: 2, resource: {url, …}, accepts: [{scheme: "exact", network: "eip155:8453", amount: "250000", asset: <USDC>, payTo, maxTimeoutSeconds, extra: {name, version}}]}:

  • the price is accepts[0].amount ÷ asset decimals (USDC: 6). At or under x402_max_usd (default $1) the client signs an EIP-3009 transferWithAuthorization for exactly that amount to payTo (EIP-712 domain from extra.name / extra.version) and retries the same request once with PAYMENT-SIGNATURE = base64 {x402Version: 2, accepted: <accepts[0] verbatim>, payload: {signature, authorization}};
  • over the cap, without a wallet, or for a scheme other than exact, it raises PaymentRequired(price_usd, requirements) (a LangbotAPIError with status 402) and signs nothing;
  • a 402 that is not x402-shaped (a plan gate) is the usual LangbotAPIError; a failed settlement (402 + PAYMENT-RESPONSE {success: false}) is a LangbotAPIError "payment failed: " and is never retried;
  • the feed's PAYMENT-RESPONSE settlement is decoded into client.last_payment["settlement"].

CLI: langbot --x402-wallet-env LANGBOT_X402_WALLET replay ev_… reads the key from that env var (never from an argument; LANGBOT_X402_WALLET is used when set even without the flag), --x402-max-usd sets the cap, and a paid call prints paid $0.5000 USDC on eip155:8453 … on stderr. The key is never logged, printed or stored; only signatures leave the process.

Configuration

~/.config/langbot/config.toml (written by langbot init), overridden by env:

LANGBOT_API_URL, LANGBOT_API_TOKEN
KALSHI_KEY_ID, KALSHI_PRIVATE_KEY_PATH, KALSHI_ENV=demo|prod
LANGBOT_MAX_POSITION_USD, LANGBOT_MAX_DAILY_LOSS_USD, LANGBOT_MARKET_ALLOWLIST
DISCORD_WEBHOOK_URL, SLACK_WEBHOOK_URL, TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
LANGBOT_I_UNDERSTAND_LIVE=1   # required for --mode live (`langbot live run --env prod` uses --yes-real-money)
LANGBOT_LIVE_CONFIG            # live limits file (default ~/.config/langbot/live.toml)
LANGBOT_LIVE_DIR               # HALT flag + audit logs (default ~/.config/langbot/live)
LANGBOT_X402_WALLET            # hex wallet key for paying per call (langbot-sdk[x402])

Local daemon

langbot run serves http://127.0.0.1:8787: GET /status, GET /strategies, POST /strategies {path, mode}, POST /strategies/{id}/start|stop, GET /positions, GET /fills. The MCP server's run_paper tool talks to it.

Kalshi notes

Prices are dollars (0.42), sent to Kalshi as fixed-point strings ("0.4200"); counts are contracts ("10.00"). Orders are bids/asks on the YES book. Signing message is timestamp_ms + METHOD + path where path includes /trade-api/v2 and no query string.

Install from source

From a checkout of this repository (Python 3.12+, uv):

uv sync --all-packages                     # every workspace package into .venv
uv run langbot --help
uv run python packages/sdk-py/examples/recent_hits.py
# or into another project's environment:
pip install ./packages/sdk-py

Develop

uv run --package langbot-sdk pytest packages/sdk-py -q
uv run ruff check packages/sdk-py

MIT. The SDK, CLI and runtime are public so you can read exactly what runs on your machine with your keys; the hosted feed is a separate, proprietary service at langbot.ai.

Metadata

Release files for langbot-sdk 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for langbot-sdk 0.1.0
File Size Uploaded Provenance Actions
langbot_sdk-0.1.0.tar.gz 198.5 kB Without provenance Details for langbot_sdk-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for langbot-sdk 0.1.0
File Interpreter ABI Platform Actions
langbot_sdk-0.1.0-py3-none-any.whl Python 3 none any Details for langbot_sdk-0.1.0-py3-none-any.whl

Total release size: 421.9 kB

Release files / langbot_sdk-0.1.0.tar.gz

Download URL langbot_sdk-0.1.0.tar.gz
Size 198.5 kB
Tags Source
SHA-256 checksum
How to use checksums
ce86471ce5aded2d90cac3b3a3ba7a9b60681444413e004b32cdb4e1c8c087b7
BLAKE2b-256 checksum
How to use checksums
ae9068453858db199abdb3e63150ba6fc07a3b852ef1135c6d74b09fd1b5b022
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / langbot_sdk-0.1.0-py3-none-any.whl

Download URL langbot_sdk-0.1.0-py3-none-any.whl
Size 223.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7b75a0062a1c16a86b97c41d3debe2ab773cc301d42774108c4fff1acb0ca50b
BLAKE2b-256 checksum
How to use checksums
4154e42da8670dbdc69e7c1eff48fe2c9c1a9efaea9678653b6b9a0631479ac7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for langbot-sdk 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page