langbot-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. langbot_sdk-0.1.0/.gitignore +8 -0
  2. langbot_sdk-0.1.0/LICENSE +21 -0
  3. langbot_sdk-0.1.0/PKG-INFO +410 -0
  4. langbot_sdk-0.1.0/README.md +368 -0
  5. langbot_sdk-0.1.0/pyproject.toml +77 -0
  6. langbot_sdk-0.1.0/src/langbot/__init__.py +147 -0
  7. langbot_sdk-0.1.0/src/langbot/adapter.py +310 -0
  8. langbot_sdk-0.1.0/src/langbot/backtest.py +272 -0
  9. langbot_sdk-0.1.0/src/langbot/cli.py +2785 -0
  10. langbot_sdk-0.1.0/src/langbot/client.py +1490 -0
  11. langbot_sdk-0.1.0/src/langbot/config.py +144 -0
  12. langbot_sdk-0.1.0/src/langbot/kalshi.py +890 -0
  13. langbot_sdk-0.1.0/src/langbot/lab/__init__.py +90 -0
  14. langbot_sdk-0.1.0/src/langbot/lab/base.py +346 -0
  15. langbot_sdk-0.1.0/src/langbot/lab/engine.py +228 -0
  16. langbot_sdk-0.1.0/src/langbot/lab/env.py +151 -0
  17. langbot_sdk-0.1.0/src/langbot/lab/fees.py +310 -0
  18. langbot_sdk-0.1.0/src/langbot/lab/history.py +42 -0
  19. langbot_sdk-0.1.0/src/langbot/lab/paper.py +351 -0
  20. langbot_sdk-0.1.0/src/langbot/lab/payrolls.py +257 -0
  21. langbot_sdk-0.1.0/src/langbot/lab/scorecard.py +383 -0
  22. langbot_sdk-0.1.0/src/langbot/lab/strategies.py +524 -0
  23. langbot_sdk-0.1.0/src/langbot/live/__init__.py +76 -0
  24. langbot_sdk-0.1.0/src/langbot/live/audit.py +108 -0
  25. langbot_sdk-0.1.0/src/langbot/live/guard.py +782 -0
  26. langbot_sdk-0.1.0/src/langbot/live/kalshi_venue.py +124 -0
  27. langbot_sdk-0.1.0/src/langbot/live/killswitch.py +95 -0
  28. langbot_sdk-0.1.0/src/langbot/live/ledger.py +304 -0
  29. langbot_sdk-0.1.0/src/langbot/live/limits.py +235 -0
  30. langbot_sdk-0.1.0/src/langbot/live/polymarket_venue.py +141 -0
  31. langbot_sdk-0.1.0/src/langbot/live/venue.py +171 -0
  32. langbot_sdk-0.1.0/src/langbot/notify.py +151 -0
  33. langbot_sdk-0.1.0/src/langbot/polymarket_trading.py +879 -0
  34. langbot_sdk-0.1.0/src/langbot/py.typed +0 -0
  35. langbot_sdk-0.1.0/src/langbot/receipts.py +662 -0
  36. langbot_sdk-0.1.0/src/langbot/runtime.py +650 -0
  37. langbot_sdk-0.1.0/src/langbot/schemas.py +1233 -0
  38. langbot_sdk-0.1.0/src/langbot/sim/__init__.py +153 -0
  39. langbot_sdk-0.1.0/src/langbot/sim/bench.py +297 -0
  40. langbot_sdk-0.1.0/src/langbot/sim/engine.py +1389 -0
  41. langbot_sdk-0.1.0/src/langbot/sim/local.py +112 -0
  42. langbot_sdk-0.1.0/src/langbot/sim/scorecard.py +390 -0
  43. langbot_sdk-0.1.0/src/langbot/sim/stats.py +148 -0
  44. langbot_sdk-0.1.0/src/langbot/sim/strategies.py +367 -0
  45. langbot_sdk-0.1.0/src/langbot/sim/study.py +581 -0
  46. langbot_sdk-0.1.0/src/langbot/strategy.py +691 -0
  47. langbot_sdk-0.1.0/src/langbot/x402.py +276 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .env.*
5
+ .venv/
6
+ dist/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Langbot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,410 @@
1
+ Metadata-Version: 2.5
2
+ Name: langbot-sdk
3
+ Version: 0.1.0
4
+ Summary: Langbot SDK and CLI: data and simulation for every market on Kalshi and Polymarket. Market history, replays against recorded order books, a strategy runtime and a Kalshi adapter.
5
+ Project-URL: Homepage, https://langbot.ai
6
+ Project-URL: Documentation, https://langbot.ai/docs
7
+ Author-email: Langbot <hello@langbot.ai>
8
+ Maintainer-email: Langbot <hello@langbot.ai>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backtesting,kalshi,mcp,order-book,polymarket,prediction-markets,trading-agents
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Office/Business :: Financial :: Investment
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: cryptography>=42
24
+ Requires-Dist: fastapi>=0.110
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: pydantic>=2.7
27
+ Requires-Dist: python-ulid>=2.2
28
+ Requires-Dist: rich>=13
29
+ Requires-Dist: typer>=0.12
30
+ Requires-Dist: uvicorn>=0.29
31
+ Requires-Dist: websockets>=13
32
+ Provides-Extra: dev
33
+ Requires-Dist: jsonschema>=4.21; extra == 'dev'
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
35
+ Requires-Dist: pytest>=8; extra == 'dev'
36
+ Requires-Dist: respx>=0.21; extra == 'dev'
37
+ Requires-Dist: ruff>=0.5; extra == 'dev'
38
+ Provides-Extra: x402
39
+ Requires-Dist: eth-account>=0.13; extra == 'x402'
40
+ Requires-Dist: x402[evm]>=2.26; extra == 'x402'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # langbot-sdk
44
+
45
+ Python SDK and CLI for [Langbot](https://langbot.ai), the data and simulation layer for every
46
+ market on Kalshi and Polymarket. Look up any market and its price history, replay a strategy
47
+ against the order books Langbot recorded, with real fees and latency, then run the same file
48
+ live with your own Kalshi keys: paper first, live only when you say so.
49
+
50
+ ```bash
51
+ pip install langbot-sdk # Python 3.12+; the import name and the command are `langbot`
52
+ uv tool install langbot-sdk # or: just the `langbot` command, isolated
53
+ ```
54
+
55
+ The PyPI name `langbot` belongs to an unrelated project; install `langbot-sdk`.
56
+
57
+ **No token needed** for the first step:
58
+
59
+ ```bash
60
+ langbot markets senate # search every Kalshi and Polymarket market
61
+ langbot market KXEARNINGSMENTIONGS-26OCT13-PRIV # one market: price history, result, replayable books
62
+ langbot events # live and upcoming events with markets
63
+ ```
64
+
65
+ With a free token (`langbot login`):
66
+
67
+ ```bash
68
+ langbot sim events # events with recorded books
69
+ langbot sim run -e ev_kxearningsmentiongs_26oct13 -s my_strategy.py # replay, scorecard
70
+ langbot run my_strategy.py --mode demo # the same file, paper, on the live feed
71
+ langbot calendar --days 7 # upcoming events, speakers, rights
72
+ langbot context KXFEDMENTION-26OCT-TARIFF # rules + book + base rate + event + mentions
73
+ langbot rates --speaker powell --phrase transitory [--series KXFEDMENTION] [--window-days 365]
74
+ langbot outcomes --speaker powell --since 2026-01-01 # settled markets, paginated (--cursor)
75
+ ```
76
+
77
+ Every command takes `--json` for the raw response.
78
+
79
+ ## Client
80
+
81
+ Runs as is, without a token (anonymous calls get the Free delay, +60 s):
82
+
83
+ ```python
84
+ import asyncio
85
+
86
+ from langbot import Langbot # alias of LangbotClient
87
+
88
+
89
+ async def main() -> None:
90
+ # token: LANGBOT_API_TOKEN, else the one `langbot login` saved, else anonymous
91
+ # api_url: LANGBOT_API_URL, else https://api.langbot.ai (stream: wss://api.langbot.ai)
92
+ async with Langbot() as lb:
93
+ hits = await lb.recent_events(types=["phrase_detected"], limit=5)
94
+ for e in hits:
95
+ print(e.event_id, e.phrase, e.t_spoken, e.verdict.counts if e.verdict else None)
96
+ if hits:
97
+ page = await lb.transcript(hits[-1].event_id) # words, text, last_seq
98
+ print(page.text[:200] or f"words withheld for {page.redacted_sources}")
99
+ async for evt in lb.stream(types=["phrase_detected"]):
100
+ print(evt.phrase, evt.t_spoken, evt.lag_vs_public_ms)
101
+
102
+
103
+ asyncio.run(main())
104
+ ```
105
+
106
+ `transcript()` returns one page (`words`, `text`, `last_seq`, `held`, `delay_ms`,
107
+ `redacted_sources`); poll with `since=page.last_seq`. Restricted sources return no words.
108
+
109
+ Feed errors raise `LangbotAPIError` (`.status`, `.detail`, `.url`); `LangbotApiError` is the
110
+ same class under the TypeScript SDK's spelling. A stream refused on policy (close `1008`: your
111
+ plan's connection cap) raises `StreamRefused` instead of reconnecting. A local feed:
112
+ `Langbot("http://localhost:8000")`. Examples that run against the hosted feed:
113
+ [`examples/`](examples/) (`recent_hits.py`, `transcript_pages.py`, `stream_hits.py`).
114
+
115
+ ### Same calls in the TypeScript SDK
116
+
117
+ The TypeScript SDK uses the Python SDK's names in camelCase (`recent_events` → `recentEvents`).
118
+ The old TypeScript names (`getEvent`, `getTranscript`, `listMarkets`, `listSubscriptions`,
119
+ `listTokens`) still work for one release, marked deprecated.
120
+
121
+ | | Python (`langbot`) | TypeScript (`@langbotai/sdk`) |
122
+ |---|---|---|
123
+ | client | `LangbotClient` / `Langbot` | `LangbotClient` / `Langbot` |
124
+ | live + upcoming events | `live_events()` | `liveEvents()` |
125
+ | one event | `event(id)` | `event(id)` |
126
+ | recent hits (one page) | `recent_events(event_id, phrases=, types=, since=, until=, cursor=, limit=)` | `recentEvents({eventId, phrases, types, since, until, cursor, limit})` |
127
+ | every page, newest first | `iter_recent_events(...)` | `iterRecentEvents({...})` |
128
+ | transcript page | `transcript(id, since=)` → `Transcript` | `transcript(id, {since})` → `Transcript` |
129
+ | every market (no token) | `public_markets(q, venue=, status=, category=, limit=)` | `publicMarkets({q, venue, status, category, limit})` |
130
+ | one market + history (no token) | `public_market(ticker)` | `publicMarket(ticker)` |
131
+ | markets with recorded events | `markets(q=, venue=, status=)` | `markets({q, venue, status})` |
132
+ | subscriptions / tokens | `subscriptions()`, `tokens()` | `subscriptions()`, `tokens()` |
133
+ | live stream | `stream(...)`, `stream_sse(...)` | `stream(...)`, `streamSSE(...)` |
134
+ | feed error | `LangbotAPIError` (`.status`, `.code`, `.detail`, `.url`) | `LangbotAPIError` (`.status`, `.code`, `.detail`, `.url`, `.body`) |
135
+ | refused stream (1008) | raises `StreamRefused` (`.code`) | `onStatus({state: "closed", code: 1008, willRetry: false})` after a `server_error` |
136
+ | token default | `LANGBOT_API_TOKEN`, else `~/.config/langbot/config.toml` | `LANGBOT_API_TOKEN` (the npm CLI also reads `config.toml`) |
137
+ | config file (both CLIs) | `~/.config/langbot/config.toml`, `LANGBOT_CONFIG` | the same file (falls back to the old `config.json`) |
138
+
139
+ Both read unknown keys and unknown enum values without failing, and both are tested against
140
+ the feed's own response shapes (`packages/schemas/fixtures/feed-responses.json`).
141
+
142
+ ## History, calendar and context
143
+
144
+ ```python
145
+ from langbot import LangbotClient
146
+
147
+ async with LangbotClient() as lb:
148
+ # list[CalendarEntry]: .rights, .speaker_keys, .phrases, .restricted
149
+ cal = await lb.calendar(days=7)
150
+ # MarketContext: .market .book .base_rates .event .mentions .rules
151
+ ctx = await lb.context("KXFEDMENTION-26OCT-TARIFF")
152
+ # BaseRates: .yes_rate (P(YES)), .markets (sample), .mispricing (avg result - open)
153
+ br = await lb.base_rates(speaker="powell", phrase="transitory", window_days=365)
154
+ # OutcomesPage: .items (list[MarketOutcome]), .next_cursor
155
+ page = await lb.outcomes(speaker="powell", limit=50)
156
+ # list[Mention]: phrase facts only, never transcript text
157
+ facts = await lb.mentions(phrase="blackwell", since=1_760_000_000_000)
158
+ who = await lb.speakers(q="pow") # list[SpeakerProfile]
159
+ ```
160
+
161
+ Base rates and outcomes are the venue's public settlement record; mentions are Langbot's own
162
+ `phrase_detected` facts. For `restricted` / `unknown` sources only phrase facts are ever returned
163
+ (no transcript text) — `CalendarEntry.restricted` tells you ahead of time.
164
+
165
+ ## Write a strategy
166
+
167
+ One protocol for `langbot run`, `langbot backtest` and `langbot sim run`: a class with a
168
+ synchronous `on_event(evt, ctx)` (the sim protocol below; `langbot.adapter` drives it live).
169
+
170
+ ```python
171
+ class Mention:
172
+ markets = ["KXEARNINGSMENTIONNVDA-*"] # glob over tickers
173
+ phrases = ["blackwell"]
174
+
175
+ def on_event(self, evt, ctx):
176
+ if evt["type"] == "phrase_detected" and evt.get("phrase") == "blackwell":
177
+ for ticker in evt["market_refs"]:
178
+ book = ctx.book(ticker)
179
+ if book and book.yes_ask and book.yes_ask < 0.60:
180
+ ctx.buy(ticker, "yes", 10, limit=book.yes_ask, tif="ioc")
181
+ ```
182
+
183
+ `ctx`: `now`, `tickers`, `book(ticker)`, `position(ticker)`, `orders(ticker)`,
184
+ `buy(ticker, side, qty, limit=None, tif=None)`, `sell(...)`, `cancel_all(ticker=None)`.
185
+ The older `class X(langbot.Strategy)` with async hooks still runs, with a deprecation warning.
186
+
187
+ Every real order passes through the **live guard** (`langbot.live`): hard limits from
188
+ `~/.config/langbot/live.toml` (per order, per market, total exposure, open orders, orders per
189
+ minute, daily loss, price through the book, series allow/deny; a missing file means the
190
+ smallest limits), a kill switch (`langbot live halt` / `resume`, plus automatic halts on daily
191
+ loss, reconciliation mismatch, repeated API errors, stale books and clock skew), reconciliation
192
+ against Kalshi every 15 s, idempotent client order ids, and an append-only audit log.
193
+
194
+ ```bash
195
+ langbot live run my_strategy.py --dry-run # Kalshi demo, nothing sent
196
+ langbot live run my_strategy.py # Kalshi demo
197
+ langbot live run my_strategy.py --env prod --yes-real-money
198
+ langbot live halt --cancel-orders # stop now, pull resting orders
199
+ ```
200
+
201
+ Demo is the default. Prod needs `--yes-real-money` and a non-empty `series_allow`. See
202
+ `docs/risk.md` for every limit and halt.
203
+
204
+ ## Langbot Sim: test a bot against a past event
205
+
206
+ `langbot.sim` replays a recorded event through your strategy against the Kalshi order books we
207
+ recorded while it happened (1 s snapshots), then scores it. Your code runs on your machine; the
208
+ feed serves the timeline (`GET /v1/sim/events/{id}/timeline`) and only ever runs its own
209
+ built-in strategies (`POST /v1/sim/runs`). Both use this same engine, so the same strategy on
210
+ the same event gives the same scorecard locally and on the server.
211
+
212
+ ```python
213
+ # my_bot.py
214
+ class FirstHit:
215
+ name = "first_hit"
216
+
217
+ def __init__(self, qty=10, max_price=0.90):
218
+ self.qty, self.max_price = qty, max_price
219
+
220
+ def on_event(self, evt, ctx): # every feed event, as the stream sends it
221
+ if evt["type"] != "phrase_detected":
222
+ return
223
+ if (evt.get("verdict") or {}).get("counts") is False: # §21: does not settle
224
+ return
225
+ for ticker in evt["market_refs"]:
226
+ if not ctx.position(ticker) and not ctx.orders(ticker):
227
+ ctx.buy(ticker, "yes", self.qty, limit=self.max_price, tif="ioc")
228
+
229
+ # optional: def on_book(self, ticker, book, ctx), on_fill(self, fill, ctx),
230
+ # on_start(self, ctx), on_end(self, ctx)
231
+ ```
232
+
233
+ ```bash
234
+ langbot sim events # what can be replayed, with coverage
235
+ langbot sim run --event ev_kxearningsmentiongs_26oct13 \
236
+ --strategy my_bot.py:FirstHit --latency 250,2000,5000 --params '{"qty": 5}'
237
+ langbot sim run --event ev_… --strategy my_bot.py --timeline gs.ndjson # cache, then offline
238
+ langbot sim strategies # the built-ins
239
+ langbot sim run --event ev_… --strategy fade_rule_mismatch --server # a built-in, on the feed
240
+ langbot research bench --strategy my_bot.py --latency 250,2250 # every recorded event, one document
241
+ langbot research calibration --speaker powell --basis mid # openings vs settlements, with intervals
242
+ ```
243
+
244
+ Hooks are synchronous (no `await`); the clock is `ctx.now` (ms). `ctx.buy(ticker, side="yes"|"no",
245
+ qty, limit=None, tif=None)` / `ctx.sell(...)` return an `Order`; `ctx.position(ticker)` (`.yes`,
246
+ `.no`, `.net`), `ctx.book(ticker)` (`yes_bid`, `yes_ask`, `no_bid`, `no_ask`, `mid`, `bids`,
247
+ `asks`), `ctx.orders(ticker)`, `ctx.cancel(order_id)`, `ctx.cash`, `ctx.tickers`. Strategies never
248
+ see settlement results.
249
+
250
+ **Fill model** (`langbot/sim/engine.py` has the details):
251
+
252
+ - An order placed at `ctx.now` arrives at `now + latency` and matches the first snapshot at or
253
+ after that, walking its depth levels; size that runs out leaves a partial fill. A market order
254
+ (no `limit`) or `tif="ioc"` cancels the rest; a limit order rests.
255
+ - A resting order fills only when a later snapshot's best opposite price moves *through* its
256
+ limit (not on touch), at the limit, up to the size shown through it.
257
+ - No market impact: the recording never reacts to you (size you take is gone only within that
258
+ snapshot). `--queue-position` makes resting orders wait behind the size recorded at their price.
259
+ - Fees per order execution: `ceil(0.07 · Σ C·P·(1−P) · 100) / 100` taker; resting fills pay 0.0175.
260
+ - Positions settle at the market's result (1.00 / 0.00); a market without one is marked to its
261
+ last mid and flagged.
262
+
263
+ The scorecard: P&L (gross, fees, net), per market, orders and fills, fill rate, max drawdown,
264
+ exposure, a latency table (by default your latency, +2 s, +5 s) and the caveats that apply.
265
+
266
+ In code:
267
+
268
+ ```python
269
+ from langbot import LangbotClient
270
+ from langbot.sim import load_strategy, run_scorecard
271
+
272
+ async with LangbotClient() as lb:
273
+ timeline = await lb.sim_timeline("ev_kxearningsmentiongs_26oct13")
274
+ card = run_scorecard(timeline, load_strategy("my_bot.py:FirstHit"), latencies=[250, 2250, 5250])
275
+ print(card["pnl"], card["latency_sensitivity"])
276
+ ```
277
+
278
+ For a timeline too big to hold, stream it: `ScorecardRun(Timeline.header(event_id, markets,
279
+ booked=tickers_with_books), strategy, latencies=[...])`, then `feed(page)` for each page of
280
+ `TimelineItem`s in timeline order and `finish()`. Every latency runs in the same pass, memory
281
+ stays at a book per ticker plus your orders and fills, and the scorecard is identical to
282
+ `run_scorecard` on the whole timeline. The feed's server runs work this way.
283
+
284
+ **Event study.** How do prices move after the speaker says a word? `langbot sim study` pools
285
+ every trigger (detected phrases, and transcript words when you pass `--words`; multi-word phrases
286
+ match consecutive words) across events and reports, per (word, ticker), the mean / median / stdev
287
+ move of the YES mid in cents and the share up / down / flat at each horizon, plus a chronological
288
+ train/test split and caveats (small n, how many pairs you tested):
289
+
290
+ ```bash
291
+ langbot sim study --event ev_a --event ev_b --words "tariff,rate cut" --horizons 5,30,300
292
+ langbot sim study --event ev_a --words tariff --tickers KX… --clock spoken --json
293
+ ```
294
+
295
+ Triggers are timed by default when the data was emitted (`--clock emitted`, plus `--delay-ms`),
296
+ i.e. when you could have traded. `--clock spoken` uses when the words were said and is not
297
+ tradeable: the broadcast can run 20-30 s behind the market, so it credits words with moves that
298
+ already happened. In code: `event_study(timeline, words=[...], tickers=None)` or
299
+ `event_study_many([tl_a, tl_b], ...)` from `langbot.sim`.
300
+
301
+ Plans: every plan runs Sim on any recorded event. Free has 20 runs a month, Builder 100,
302
+ Standard and up unlimited. A run is one server run or one timeline download (its first page).
303
+ Past the allowance the feed answers `429` with `code == "quota_exceeded"`; the error's `.quota`
304
+ says what was used and when it resets (`resets_at`).
305
+
306
+ ## Modules
307
+
308
+ | module | what |
309
+ |---|---|
310
+ | `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()` |
311
+ | `langbot.client` | `Langbot` / `LangbotClient` — every feed endpoint incl. `calendar()`, `context()`, `base_rates()`, `outcomes()`, `mentions()`, `speakers()`; `stream()` (WS, auto-reconnect), `stream_sse()` |
312
+ | `langbot.kalshi` | `KalshiClient` — Trade API v2 with RSA-PSS / Ed25519 signing, token-bucket rate limiter, `ws_subscribe()` with orderbook reconstruction |
313
+ | `langbot.strategy` | `Strategy`, `RiskRails`, `PaperBroker`, `KalshiBroker`, `Position` |
314
+ | `langbot.live` | `LiveGuard`, `GuardedBroker`, `Limits`/`load_limits`, `KillSwitch`, `AuditLog`, the `Venue` protocol, `KalshiVenue`, `PolymarketUSVenue` |
315
+ | `langbot.runtime` | `langbot run`: feed stream + Kalshi books + local daemon on `127.0.0.1:8787` |
316
+ | `langbot.backtest` | replay archived events + book snapshots (feed or JSONL) at original timestamps |
317
+ | `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 |
318
+ | `langbot.notify` | Discord / Slack / Telegram notifiers (`notifier_from_env()`) |
319
+ | `langbot.cli` | the `langbot` command |
320
+
321
+ ## Paying per call
322
+
323
+ Some endpoints are sold by the call (evidence packs, provenance reports, replay bundles,
324
+ history, facts — CONTRACTS §17). Without a plan that covers them the feed answers
325
+ `402 Payment Required` in the [x402](https://x402.org) v2 shape, and the client can pay it in
326
+ USDC with a wallet you hold:
327
+
328
+ ```bash
329
+ pip install "langbot-sdk[x402]" # adds the official x402 client + eth-account (pulls web3)
330
+ export LANGBOT_X402_WALLET=0x… # hex private key of a funded wallet; yours, never Langbot's
331
+ ```
332
+
333
+ ```python
334
+ import os
335
+ from langbot import LangbotClient, PaymentRequired
336
+
337
+ async with LangbotClient(x402_wallet=os.environ["LANGBOT_X402_WALLET"], x402_max_usd=1.0) as lb:
338
+ bundle = await lb.replay("ev_nvda_q3_2026") # 402 → signed retry → 200, one call
339
+ print(lb.last_payment) # {price_usd, header, network, settlement: {transaction, payer, …}}
340
+ ```
341
+
342
+ What happens on a 402 whose `PAYMENT-REQUIRED` header and JSON body carry the x402 v2
343
+ `PaymentRequired` object `{x402Version: 2, resource: {url, …}, accepts: [{scheme: "exact",
344
+ network: "eip155:8453", amount: "250000", asset: <USDC>, payTo, maxTimeoutSeconds, extra: {name, version}}]}`:
345
+
346
+ - the price is `accepts[0].amount` ÷ asset decimals (USDC: 6). At or under `x402_max_usd`
347
+ (default **$1**) the client signs an EIP-3009 `transferWithAuthorization` for exactly that
348
+ amount to `payTo` (EIP-712 domain from `extra.name` / `extra.version`) and retries the same
349
+ request **once** with `PAYMENT-SIGNATURE` = base64 `{x402Version: 2, accepted: <accepts[0]
350
+ verbatim>, payload: {signature, authorization}}`;
351
+ - over the cap, without a wallet, or for a scheme other than `exact`, it raises
352
+ `PaymentRequired(price_usd, requirements)` (a `LangbotAPIError` with status 402) and signs
353
+ nothing;
354
+ - a 402 that is not x402-shaped (a plan gate) is the usual `LangbotAPIError`; a failed
355
+ settlement (402 + `PAYMENT-RESPONSE {success: false}`) is a `LangbotAPIError`
356
+ "payment failed: <reason>" and is never retried;
357
+ - the feed's `PAYMENT-RESPONSE` settlement is decoded into `client.last_payment["settlement"]`.
358
+
359
+ CLI: `langbot --x402-wallet-env LANGBOT_X402_WALLET replay ev_…` reads the key from that env
360
+ var (never from an argument; `LANGBOT_X402_WALLET` is used when set even without the flag),
361
+ `--x402-max-usd` sets the cap, and a paid call prints `paid $0.5000 USDC on eip155:8453 …` on stderr.
362
+ The key is never logged, printed or stored; only signatures leave the process.
363
+
364
+ ## Configuration
365
+
366
+ `~/.config/langbot/config.toml` (written by `langbot init`), overridden by env:
367
+
368
+ ```
369
+ LANGBOT_API_URL, LANGBOT_API_TOKEN
370
+ KALSHI_KEY_ID, KALSHI_PRIVATE_KEY_PATH, KALSHI_ENV=demo|prod
371
+ LANGBOT_MAX_POSITION_USD, LANGBOT_MAX_DAILY_LOSS_USD, LANGBOT_MARKET_ALLOWLIST
372
+ DISCORD_WEBHOOK_URL, SLACK_WEBHOOK_URL, TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
373
+ LANGBOT_I_UNDERSTAND_LIVE=1 # required for --mode live (`langbot live run --env prod` uses --yes-real-money)
374
+ LANGBOT_LIVE_CONFIG # live limits file (default ~/.config/langbot/live.toml)
375
+ LANGBOT_LIVE_DIR # HALT flag + audit logs (default ~/.config/langbot/live)
376
+ LANGBOT_X402_WALLET # hex wallet key for paying per call (langbot-sdk[x402])
377
+ ```
378
+
379
+ ## Local daemon
380
+
381
+ `langbot run` serves `http://127.0.0.1:8787`: `GET /status`, `GET /strategies`,
382
+ `POST /strategies` `{path, mode}`, `POST /strategies/{id}/start|stop`, `GET /positions`,
383
+ `GET /fills`. The MCP server's `run_paper` tool talks to it.
384
+
385
+ ## Kalshi notes
386
+
387
+ Prices are dollars (`0.42`), sent to Kalshi as fixed-point strings (`"0.4200"`); counts
388
+ are contracts (`"10.00"`). Orders are bids/asks on the **YES** book. Signing message is
389
+ `timestamp_ms + METHOD + path` where `path` includes `/trade-api/v2` and no query string.
390
+
391
+ ## Install from source
392
+
393
+ From a checkout of this repository (Python 3.12+, [uv](https://docs.astral.sh/uv/)):
394
+
395
+ ```bash
396
+ uv sync --all-packages # every workspace package into .venv
397
+ uv run langbot --help
398
+ uv run python packages/sdk-py/examples/recent_hits.py
399
+ # or into another project's environment:
400
+ pip install ./packages/sdk-py
401
+ ```
402
+
403
+ ## Develop
404
+
405
+ ```bash
406
+ uv run --package langbot-sdk pytest packages/sdk-py -q
407
+ uv run ruff check packages/sdk-py
408
+ ```
409
+
410
+ 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](https://langbot.ai).