You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Surfaced by the multi-LLM review of PR #403 (perps/margin API branch). PerpsConfig already declares separate _PERPS_REST_HOSTS / _PERPS_WS_HOSTS frozensets, but the host allowlist check and the path checks do not actually use that split, so a REST URL on a WS host (or vice versa) is accepted and ws_base_url is never path-validated.
Problem
In kalshi/perps/config.py:
_PERPS_KNOWN_HOSTS = _PERPS_REST_HOSTS | _PERPS_WS_HOSTS (line 39) is the union of both host sets. _validate_perps_url is the single helper used for both fields and checks host not in _PERPS_KNOWN_HOSTS (line 172). It is called for base_url (lines 84-90) and ws_base_url (lines 91-97) with the same union. The _PERPS_REST_HOSTS / _PERPS_WS_HOSTS split (lines 33-38) is therefore dead for per-field host validation — only the prod/demo-pair guard (lines 108-120) consumes the individual host constants.
The /trade-api/v2 path check (lines 100-105) is applied only to base_url. There is no equivalent check enforcing the perps WS path (/trade-api/ws/v2/margin) on ws_base_url.
Because the perps REST and WS hosts genuinely differ (external-api.kalshi.com vs external-api-margin-ws.kalshi.com) — unlike the prediction API where one host serves both — the union allowlist lets a caller cross the wires without the guard firing. Verified empirically against current code:
# REST base_url pointed at the WS host — accepted (passes union host check + /trade-api/v2 path):
PerpsConfig(base_url='https://external-api-margin-ws.kalshi.com/trade-api/v2', ws_base_url=<prod ws>)
-> ACCEPTED
# ws_base_url pointed at the REST host with an arbitrary path — accepted (union host check, no WS path check):
PerpsConfig(base_url=<prod rest>, ws_base_url='wss://external-api.kalshi.com/anything/here')
-> ACCEPTED
# ws_base_url on the correct WS host but a wrong path — accepted (no WS path check at all):
PerpsConfig(base_url=<prod rest>, ws_base_url='wss://external-api-margin-ws.kalshi.com/totally/wrong/path')
-> ACCEPTED
Concrete failure: a typo or copy-paste that swaps the REST and WS endpoints (e.g. a REST base_url aimed at the margin-WS host, or a WS ws_base_url aimed at the REST host) passes construction silently and only fails later at connect/request time with an opaque transport error, instead of being caught at config construction where every other perps URL footgun is rejected. The host allowlist is a security guard (it gates where signed, API-key-bearing requests are sent); allowing the wrong perps host through it weakens that guard for the perps surface.
Proposed fix
Minimal, surgical change in kalshi/perps/config.py:
Pass the field-specific host set into _validate_perps_url instead of the union. Add a known_hosts: frozenset[str] parameter and call it with _PERPS_REST_HOSTS for base_url and _PERPS_WS_HOSTS for ws_base_url. Update the error message / log to reference the passed set. (The union _PERPS_KNOWN_HOSTS can then be dropped, or kept only if referenced elsewhere.)
Add a ws_base_url path check mirroring the REST one: after trailing-slash stripping, assert urlparse(self.ws_base_url).path == "/trade-api/ws/v2/margin" and raise a ValueError naming the field, the expected path, and the actual ws_base_url when it does not match. Keep the allow_unknown_host escape hatch governing only the host check, consistent with the REST side.
Acceptance criteria
base_url is validated against _PERPS_REST_HOSTS only; a perps WS host (external-api-margin-ws.kalshi.com / .demo.kalshi.co) as base_url raises ValueError unless allow_unknown_host is set.
ws_base_url is validated against _PERPS_WS_HOSTS only; a perps REST host (external-api.kalshi.com / .demo.kalshi.co) as ws_base_url raises ValueError unless allow_unknown_host is set.
ws_base_url must have path /trade-api/ws/v2/margin; a valid WS host with a wrong path raises ValueError.
PerpsConfig.production() / PerpsConfig.demo() and the existing localhost case (ws://localhost/trade-api/ws/v2/margin) still construct successfully — note localhost is gated by _LOCAL_HOSTS, so confirm the new WS path check still allows it (the canonical localhost WS URL already uses the expected path).
Regression tests in tests/perps/test_config.py:
base_url on a WS host is rejected.
ws_base_url on a REST host is rejected.
ws_base_url with a wrong path on a valid WS host is rejected.
Tradeoff / behavior change: this makes previously-accepted (mis)configurations raise at construction. That is the intended hardening, but it is technically stricter — it is not a public output/format change, so it is not labeled breaking; the only configs it newly rejects are ones that were already misconfigured.
The prediction-API KalshiConfig (kalshi/config.py:27) uses a single shared _KNOWN_HOSTS because one host serves both REST and WS there, and it likewise has no dedicated WS path check (only base_url checks /trade-api/v2 at lines 112-115). Tightening the WS path on the prediction side is a separate, optional consideration — out of scope here; this issue is perps-specific because the perps hosts actually diverge.
Context
Surfaced by the multi-LLM review of PR #403 (perps/margin API branch).
PerpsConfigalready declares separate_PERPS_REST_HOSTS/_PERPS_WS_HOSTSfrozensets, but the host allowlist check and the path checks do not actually use that split, so a REST URL on a WS host (or vice versa) is accepted andws_base_urlis never path-validated.Problem
In
kalshi/perps/config.py:_PERPS_KNOWN_HOSTS = _PERPS_REST_HOSTS | _PERPS_WS_HOSTS(line 39) is the union of both host sets._validate_perps_urlis the single helper used for both fields and checkshost not in _PERPS_KNOWN_HOSTS(line 172). It is called forbase_url(lines 84-90) andws_base_url(lines 91-97) with the same union. The_PERPS_REST_HOSTS/_PERPS_WS_HOSTSsplit (lines 33-38) is therefore dead for per-field host validation — only the prod/demo-pair guard (lines 108-120) consumes the individual host constants./trade-api/v2path check (lines 100-105) is applied only tobase_url. There is no equivalent check enforcing the perps WS path (/trade-api/ws/v2/margin) onws_base_url.Because the perps REST and WS hosts genuinely differ (
external-api.kalshi.comvsexternal-api-margin-ws.kalshi.com) — unlike the prediction API where one host serves both — the union allowlist lets a caller cross the wires without the guard firing. Verified empirically against current code:Concrete failure: a typo or copy-paste that swaps the REST and WS endpoints (e.g. a REST
base_urlaimed at the margin-WS host, or a WSws_base_urlaimed at the REST host) passes construction silently and only fails later at connect/request time with an opaque transport error, instead of being caught at config construction where every other perps URL footgun is rejected. The host allowlist is a security guard (it gates where signed, API-key-bearing requests are sent); allowing the wrong perps host through it weakens that guard for the perps surface.Proposed fix
Minimal, surgical change in
kalshi/perps/config.py:_validate_perps_urlinstead of the union. Add aknown_hosts: frozenset[str]parameter and call it with_PERPS_REST_HOSTSforbase_urland_PERPS_WS_HOSTSforws_base_url. Update the error message / log to reference the passed set. (The union_PERPS_KNOWN_HOSTScan then be dropped, or kept only if referenced elsewhere.)ws_base_urlpath check mirroring the REST one: after trailing-slash stripping, asserturlparse(self.ws_base_url).path == "/trade-api/ws/v2/margin"and raise aValueErrornaming the field, the expected path, and the actualws_base_urlwhen it does not match. Keep theallow_unknown_hostescape hatch governing only the host check, consistent with the REST side.Acceptance criteria
base_urlis validated against_PERPS_REST_HOSTSonly; a perps WS host (external-api-margin-ws.kalshi.com/.demo.kalshi.co) asbase_urlraisesValueErrorunlessallow_unknown_hostis set.ws_base_urlis validated against_PERPS_WS_HOSTSonly; a perps REST host (external-api.kalshi.com/.demo.kalshi.co) asws_base_urlraisesValueErrorunlessallow_unknown_hostis set.ws_base_urlmust have path/trade-api/ws/v2/margin; a valid WS host with a wrong path raisesValueError.PerpsConfig.production()/PerpsConfig.demo()and the existinglocalhostcase (ws://localhost/trade-api/ws/v2/margin) still construct successfully — note localhost is gated by_LOCAL_HOSTS, so confirm the new WS path check still allows it (the canonical localhost WS URL already uses the expected path).tests/perps/test_config.py:base_urlon a WS host is rejected.ws_base_urlon a REST host is rejected.ws_base_urlwith a wrong path on a valid WS host is rejected.Notes
kalshi/perps/config.py).breaking; the only configs it newly rejects are ones that were already misconfigured.KalshiConfig(kalshi/config.py:27) uses a single shared_KNOWN_HOSTSbecause one host serves both REST and WS there, and it likewise has no dedicated WS path check (onlybase_urlchecks/trade-api/v2at lines 112-115). Tightening the WS path on the prediction side is a separate, optional consideration — out of scope here; this issue is perps-specific because the perps hosts actually diverge.