Skip to content

Commit c140625

Browse files
committed
fix(sdk): implement resilient authentication in Python and TypeScript SDKs
- Ensures access token is read fresh from server.lock on every request - Prevents 'Unauthorized' errors after sidecar server restarts - Updates code generators to maintain resilience in future methods
1 parent 0e4cfed commit c140625

5 files changed

Lines changed: 113 additions & 104 deletions

File tree

changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.19.5] - 2026-03-06
6+
7+
### Fixed
8+
9+
- **SDK: Resilient Authentication (Python & TypeScript)**: Eliminated "Unauthorized: Invalid or missing access token" errors caused by sidecar server restarts. Both the Python and TypeScript SDKs now read the access token fresh from the `~/.pmxt/server.lock` file on every request via a new `getAuthHeaders` helper. This ensures that if the server rebooted and rotated tokens, existing `Exchange` instances (like `Polymarket`) automatically pick up the new valid token on their next call, removing the need for developers to manually re-instantiate clients.
10+
- **SDK: Generator Persistence (Python & TypeScript)**: Updated both `sdks/python/scripts/generate-client-methods.js` and `sdks/typescript/scripts/generate-client-methods.js` to emit the live header retrieval pattern, ensuring authentication resilience is maintained in all future auto-generated methods.
11+
512
## [2.19.4] - 2026-03-06
613

714
### Added

sdks/python/pmxt/client.py

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -271,19 +271,6 @@ def __init__(
271271
config = Configuration(host=base_url)
272272
self._api_client = ApiClient(configuration=config)
273273

274-
# Add access token from lock file (with retry for timing issues)
275-
server_info = None
276-
for attempt in range(5):
277-
server_info = self._server_manager.get_server_info()
278-
if server_info and 'accessToken' in server_info:
279-
break
280-
if attempt < 4:
281-
import time
282-
time.sleep(0.1)
283-
284-
if server_info and 'accessToken' in server_info:
285-
self._api_client.default_headers['x-pmxt-access-token'] = server_info['accessToken']
286-
287274
self._api = DefaultApi(api_client=self._api_client)
288275

289276
def _handle_response(self, response: Dict[str, Any]) -> Any:
@@ -310,6 +297,19 @@ def _extract_api_error(self, e: Exception) -> str:
310297
pass
311298
return str(e)
312299

300+
def _get_auth_headers(self) -> Dict[str, str]:
301+
"""Build request headers with a fresh access token read from the lock file.
302+
303+
The token is re-read on every call so that if the sidecar server restarts
304+
(and writes a new token) existing client objects automatically recover on
305+
the next request — no re-instantiation required.
306+
"""
307+
headers: Dict[str, str] = dict(self._api_client.default_headers)
308+
server_info = self._server_manager.get_server_info()
309+
if server_info and 'accessToken' in server_info:
310+
headers['x-pmxt-access-token'] = server_info['accessToken']
311+
return headers
312+
313313
def _get_credentials_dict(self) -> Optional[Dict[str, Any]]:
314314
"""Build credentials dictionary for API requests."""
315315
if not self.api_key and not self.private_key:
@@ -344,7 +344,7 @@ def has(self) -> Dict[str, Any]:
344344
try:
345345
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/has"
346346
headers = {"Accept": "application/json"}
347-
headers.update(self._api_client.default_headers)
347+
headers.update(self._get_auth_headers())
348348
response = self._api_client.call_api(
349349
method="GET",
350350
url=url,
@@ -368,7 +368,7 @@ def _call_method(self, method_name: str, params: Optional[Dict[str, Any]] = None
368368
if creds:
369369
body["credentials"] = creds
370370
headers = {"Content-Type": "application/json", "Accept": "application/json"}
371-
headers.update(self._api_client.default_headers)
371+
headers.update(self._get_auth_headers())
372372
response = self._api_client.call_api(
373373
method="POST",
374374
url=url,
@@ -406,7 +406,7 @@ def call_api(self, operation_id: str, params: Optional[Dict[str, Any]] = None) -
406406
body["credentials"] = creds
407407

408408
headers = {"Content-Type": "application/json", "Accept": "application/json"}
409-
headers.update(self._api_client.default_headers)
409+
headers.update(self._get_auth_headers())
410410

411411
response = self._api_client.call_api(
412412
method="POST",
@@ -471,7 +471,7 @@ def fetch_markets(self, params: Optional[dict] = None) -> List[UnifiedMarket]:
471471
body["credentials"] = creds
472472
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchMarkets"
473473
headers = {"Content-Type": "application/json", "Accept": "application/json"}
474-
headers.update(self._api_client.default_headers)
474+
headers.update(self._get_auth_headers())
475475
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
476476
response.read()
477477
data = self._handle_response(json.loads(response.data))
@@ -490,7 +490,7 @@ def fetch_markets_paginated(self, params: Optional[dict] = None) -> PaginatedMar
490490
body["credentials"] = creds
491491
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchMarketsPaginated"
492492
headers = {"Content-Type": "application/json", "Accept": "application/json"}
493-
headers.update(self._api_client.default_headers)
493+
headers.update(self._get_auth_headers())
494494
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
495495
response.read()
496496
data = self._handle_response(json.loads(response.data))
@@ -513,7 +513,7 @@ def fetch_events(self, params: Optional[dict] = None) -> List[UnifiedEvent]:
513513
body["credentials"] = creds
514514
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchEvents"
515515
headers = {"Content-Type": "application/json", "Accept": "application/json"}
516-
headers.update(self._api_client.default_headers)
516+
headers.update(self._get_auth_headers())
517517
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
518518
response.read()
519519
data = self._handle_response(json.loads(response.data))
@@ -532,7 +532,7 @@ def fetch_market(self, params: Optional[dict] = None) -> UnifiedMarket:
532532
body["credentials"] = creds
533533
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchMarket"
534534
headers = {"Content-Type": "application/json", "Accept": "application/json"}
535-
headers.update(self._api_client.default_headers)
535+
headers.update(self._get_auth_headers())
536536
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
537537
response.read()
538538
data = self._handle_response(json.loads(response.data))
@@ -551,7 +551,7 @@ def fetch_event(self, params: Optional[dict] = None) -> UnifiedEvent:
551551
body["credentials"] = creds
552552
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchEvent"
553553
headers = {"Content-Type": "application/json", "Accept": "application/json"}
554-
headers.update(self._api_client.default_headers)
554+
headers.update(self._get_auth_headers())
555555
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
556556
response.read()
557557
data = self._handle_response(json.loads(response.data))
@@ -569,7 +569,7 @@ def fetch_order_book(self, id: str) -> OrderBook:
569569
body["credentials"] = creds
570570
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchOrderBook"
571571
headers = {"Content-Type": "application/json", "Accept": "application/json"}
572-
headers.update(self._api_client.default_headers)
572+
headers.update(self._get_auth_headers())
573573
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
574574
response.read()
575575
data = self._handle_response(json.loads(response.data))
@@ -587,7 +587,7 @@ def cancel_order(self, order_id: str) -> Order:
587587
body["credentials"] = creds
588588
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/cancelOrder"
589589
headers = {"Content-Type": "application/json", "Accept": "application/json"}
590-
headers.update(self._api_client.default_headers)
590+
headers.update(self._get_auth_headers())
591591
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
592592
response.read()
593593
data = self._handle_response(json.loads(response.data))
@@ -605,7 +605,7 @@ def fetch_order(self, order_id: str) -> Order:
605605
body["credentials"] = creds
606606
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchOrder"
607607
headers = {"Content-Type": "application/json", "Accept": "application/json"}
608-
headers.update(self._api_client.default_headers)
608+
headers.update(self._get_auth_headers())
609609
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
610610
response.read()
611611
data = self._handle_response(json.loads(response.data))
@@ -624,7 +624,7 @@ def fetch_open_orders(self, market_id: Optional[str] = None) -> List[Order]:
624624
body["credentials"] = creds
625625
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchOpenOrders"
626626
headers = {"Content-Type": "application/json", "Accept": "application/json"}
627-
headers.update(self._api_client.default_headers)
627+
headers.update(self._get_auth_headers())
628628
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
629629
response.read()
630630
data = self._handle_response(json.loads(response.data))
@@ -643,7 +643,7 @@ def fetch_my_trades(self, params: Optional[dict] = None) -> List[UserTrade]:
643643
body["credentials"] = creds
644644
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchMyTrades"
645645
headers = {"Content-Type": "application/json", "Accept": "application/json"}
646-
headers.update(self._api_client.default_headers)
646+
headers.update(self._get_auth_headers())
647647
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
648648
response.read()
649649
data = self._handle_response(json.loads(response.data))
@@ -662,7 +662,7 @@ def fetch_closed_orders(self, params: Optional[dict] = None) -> List[Order]:
662662
body["credentials"] = creds
663663
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchClosedOrders"
664664
headers = {"Content-Type": "application/json", "Accept": "application/json"}
665-
headers.update(self._api_client.default_headers)
665+
headers.update(self._get_auth_headers())
666666
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
667667
response.read()
668668
data = self._handle_response(json.loads(response.data))
@@ -681,7 +681,7 @@ def fetch_all_orders(self, params: Optional[dict] = None) -> List[Order]:
681681
body["credentials"] = creds
682682
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchAllOrders"
683683
headers = {"Content-Type": "application/json", "Accept": "application/json"}
684-
headers.update(self._api_client.default_headers)
684+
headers.update(self._get_auth_headers())
685685
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
686686
response.read()
687687
data = self._handle_response(json.loads(response.data))
@@ -698,7 +698,7 @@ def fetch_positions(self) -> List[Position]:
698698
body["credentials"] = creds
699699
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchPositions"
700700
headers = {"Content-Type": "application/json", "Accept": "application/json"}
701-
headers.update(self._api_client.default_headers)
701+
headers.update(self._get_auth_headers())
702702
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
703703
response.read()
704704
data = self._handle_response(json.loads(response.data))
@@ -715,7 +715,7 @@ def fetch_balance(self) -> List[Balance]:
715715
body["credentials"] = creds
716716
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/fetchBalance"
717717
headers = {"Content-Type": "application/json", "Accept": "application/json"}
718-
headers.update(self._api_client.default_headers)
718+
headers.update(self._get_auth_headers())
719719
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
720720
response.read()
721721
data = self._handle_response(json.loads(response.data))
@@ -732,7 +732,7 @@ def close(self) -> None:
732732
body["credentials"] = creds
733733
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/close"
734734
headers = {"Content-Type": "application/json", "Accept": "application/json"}
735-
headers.update(self._api_client.default_headers)
735+
headers.update(self._get_auth_headers())
736736
response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)
737737
response.read()
738738
self._handle_response(json.loads(response.data))
@@ -1416,7 +1416,7 @@ def get_execution_price_detailed(
14161416
url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/getExecutionPriceDetailed"
14171417

14181418
headers = {"Content-Type": "application/json", "Accept": "application/json"}
1419-
headers.update(self._api_client.default_headers)
1419+
headers.update(self._get_auth_headers())
14201420

14211421
response = self._api_client.call_api(
14221422
method="POST",

sdks/python/scripts/generate-client-methods.js

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@ const SKIP_GENERATE = new Set([
4343
// pattern: how to handle response data
4444
// converter: converter function name (for array/single patterns)
4545
const METHOD_RETURN_CONFIG = {
46-
fetchMarkets: { returnPy: 'List[UnifiedMarket]', pattern: 'array', converter: '_convert_market' },
47-
fetchMarketsPaginated: { returnPy: 'PaginatedMarketsResult', pattern: 'paginated' },
48-
fetchEvents: { returnPy: 'List[UnifiedEvent]', pattern: 'array', converter: '_convert_event' },
49-
fetchMarket: { returnPy: 'UnifiedMarket', pattern: 'single', converter: '_convert_market' },
50-
fetchEvent: { returnPy: 'UnifiedEvent', pattern: 'single', converter: '_convert_event' },
51-
fetchOrderBook: { returnPy: 'OrderBook', pattern: 'single', converter: '_convert_order_book' },
52-
cancelOrder: { returnPy: 'Order', pattern: 'single', converter: '_convert_order' },
53-
fetchOrder: { returnPy: 'Order', pattern: 'single', converter: '_convert_order' },
54-
fetchOpenOrders: { returnPy: 'List[Order]', pattern: 'array', converter: '_convert_order' },
55-
fetchMyTrades: { returnPy: 'List[UserTrade]', pattern: 'array', converter: '_convert_user_trade' },
56-
fetchClosedOrders: { returnPy: 'List[Order]', pattern: 'array', converter: '_convert_order' },
57-
fetchAllOrders: { returnPy: 'List[Order]', pattern: 'array', converter: '_convert_order' },
58-
fetchPositions: { returnPy: 'List[Position]', pattern: 'array', converter: '_convert_position' },
59-
fetchBalance: { returnPy: 'List[Balance]', pattern: 'array', converter: '_convert_balance' },
60-
close: { returnPy: 'None', pattern: 'void' },
46+
fetchMarkets: { returnPy: 'List[UnifiedMarket]', pattern: 'array', converter: '_convert_market' },
47+
fetchMarketsPaginated: { returnPy: 'PaginatedMarketsResult', pattern: 'paginated' },
48+
fetchEvents: { returnPy: 'List[UnifiedEvent]', pattern: 'array', converter: '_convert_event' },
49+
fetchMarket: { returnPy: 'UnifiedMarket', pattern: 'single', converter: '_convert_market' },
50+
fetchEvent: { returnPy: 'UnifiedEvent', pattern: 'single', converter: '_convert_event' },
51+
fetchOrderBook: { returnPy: 'OrderBook', pattern: 'single', converter: '_convert_order_book' },
52+
cancelOrder: { returnPy: 'Order', pattern: 'single', converter: '_convert_order' },
53+
fetchOrder: { returnPy: 'Order', pattern: 'single', converter: '_convert_order' },
54+
fetchOpenOrders: { returnPy: 'List[Order]', pattern: 'array', converter: '_convert_order' },
55+
fetchMyTrades: { returnPy: 'List[UserTrade]', pattern: 'array', converter: '_convert_user_trade' },
56+
fetchClosedOrders: { returnPy: 'List[Order]', pattern: 'array', converter: '_convert_order' },
57+
fetchAllOrders: { returnPy: 'List[Order]', pattern: 'array', converter: '_convert_order' },
58+
fetchPositions: { returnPy: 'List[Position]', pattern: 'array', converter: '_convert_position' },
59+
fetchBalance: { returnPy: 'List[Balance]', pattern: 'array', converter: '_convert_balance' },
60+
close: { returnPy: 'None', pattern: 'void' },
6161
};
6262

6363
// ---------------------------------------------------------------------------
@@ -71,22 +71,22 @@ function camelToSnake(s) {
7171
function typeNodeToPy(node, sf) {
7272
if (!node) return 'Any';
7373
switch (node.kind) {
74-
case ts.SyntaxKind.StringKeyword: return 'str';
75-
case ts.SyntaxKind.NumberKeyword: return 'float';
76-
case ts.SyntaxKind.BooleanKeyword: return 'bool';
77-
case ts.SyntaxKind.VoidKeyword: return 'None';
78-
case ts.SyntaxKind.AnyKeyword: return 'Any';
74+
case ts.SyntaxKind.StringKeyword: return 'str';
75+
case ts.SyntaxKind.NumberKeyword: return 'float';
76+
case ts.SyntaxKind.BooleanKeyword: return 'bool';
77+
case ts.SyntaxKind.VoidKeyword: return 'None';
78+
case ts.SyntaxKind.AnyKeyword: return 'Any';
7979
case ts.SyntaxKind.UndefinedKeyword: return 'Any';
80-
case ts.SyntaxKind.TypeLiteral: return 'dict';
80+
case ts.SyntaxKind.TypeLiteral: return 'dict';
8181
case ts.SyntaxKind.TypeReference: {
8282
const name = node.typeName.kind === ts.SyntaxKind.Identifier
8383
? node.typeName.text
8484
: node.typeName.right.text;
8585
if (name === 'Promise' && node.typeArguments) {
8686
return typeNodeToPy(node.typeArguments[0], sf);
8787
}
88-
if (name === 'string') return 'str';
89-
if (name === 'number') return 'float';
88+
if (name === 'string') return 'str';
89+
if (name === 'number') return 'float';
9090
if (name === 'boolean') return 'bool';
9191
return 'dict';
9292
}
@@ -225,7 +225,7 @@ function generatePyMethod(name, params, config, sf) {
225225
` body["credentials"] = creds`,
226226
` url = f"{self._api_client.configuration.host}/api/{self.exchange_name}/${name}"`,
227227
` headers = {"Content-Type": "application/json", "Accept": "application/json"}`,
228-
` headers.update(self._api_client.default_headers)`,
228+
` headers.update(self._get_auth_headers())`,
229229
` response = self._api_client.call_api(method="POST", url=url, body=body, header_params=headers)`,
230230
` response.read()`,
231231
returnLines,

0 commit comments

Comments
 (0)