polynode 0.14.0__tar.gz → 0.15.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 (50) hide show
  1. {polynode-0.14.0 → polynode-0.15.0}/PKG-INFO +176 -18
  2. {polynode-0.14.0 → polynode-0.15.0}/README.md +175 -17
  3. {polynode-0.14.0 → polynode-0.15.0}/core-contract-v1.json +55 -1
  4. polynode-0.15.0/polynode/__init__.py +161 -0
  5. polynode-0.15.0/polynode/_version.py +1 -0
  6. polynode-0.15.0/polynode/market_protocol.py +466 -0
  7. {polynode-0.14.0 → polynode-0.15.0}/polynode/redemption_watcher.py +66 -8
  8. {polynode-0.14.0 → polynode-0.15.0}/polynode/short_form.py +35 -2
  9. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/__init__.py +17 -1
  10. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/clob_api.py +12 -0
  11. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/constants.py +16 -0
  12. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/eip712.py +122 -14
  13. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/onboarding.py +186 -1
  14. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/position_management.py +93 -0
  15. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/sqlite_backend.py +37 -1
  16. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/trader.py +487 -7
  17. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/types.py +121 -9
  18. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/__init__.py +1 -0
  19. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/events.py +59 -0
  20. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/perps.py +62 -1
  21. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/short_form.py +3 -0
  22. polynode-0.15.0/polynode/types/v3.py +263 -0
  23. {polynode-0.14.0 → polynode-0.15.0}/polynode/v3.py +469 -3
  24. {polynode-0.14.0 → polynode-0.15.0}/polynode/v3_operations.py +9 -0
  25. {polynode-0.14.0 → polynode-0.15.0}/pyproject.toml +1 -1
  26. polynode-0.14.0/polynode/__init__.py +0 -83
  27. polynode-0.14.0/polynode/_version.py +0 -1
  28. {polynode-0.14.0 → polynode-0.15.0}/.gitignore +0 -0
  29. {polynode-0.14.0 → polynode-0.15.0}/core-fixtures-v1.json +0 -0
  30. {polynode-0.14.0 → polynode-0.15.0}/polynode/cache/__init__.py +0 -0
  31. {polynode-0.14.0 → polynode-0.15.0}/polynode/client.py +0 -0
  32. {polynode-0.14.0 → polynode-0.15.0}/polynode/engine.py +0 -0
  33. {polynode-0.14.0 → polynode-0.15.0}/polynode/errors.py +0 -0
  34. {polynode-0.14.0 → polynode-0.15.0}/polynode/orderbook.py +0 -0
  35. {polynode-0.14.0 → polynode-0.15.0}/polynode/orderbook_integrity.py +0 -0
  36. {polynode-0.14.0 → polynode-0.15.0}/polynode/orderbook_state.py +0 -0
  37. {polynode-0.14.0 → polynode-0.15.0}/polynode/perps.py +0 -0
  38. {polynode-0.14.0 → polynode-0.15.0}/polynode/subscription.py +0 -0
  39. {polynode-0.14.0 → polynode-0.15.0}/polynode/testing.py +0 -0
  40. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/cosigner.py +0 -0
  41. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/escrow.py +0 -0
  42. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/privy.py +0 -0
  43. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/relayer.py +0 -0
  44. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/signer.py +0 -0
  45. {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/user_relayer.py +0 -0
  46. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/enums.py +0 -0
  47. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/orderbook.py +0 -0
  48. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/rest.py +0 -0
  49. {polynode-0.14.0 → polynode-0.15.0}/polynode/types/ws.py +0 -0
  50. {polynode-0.14.0 → polynode-0.15.0}/polynode/ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: polynode
3
- Version: 0.14.0
3
+ Version: 0.15.0
4
4
  Summary: Python SDK for the Polynode real-time prediction market data platform
5
5
  Project-URL: Homepage, https://polynode.dev
6
6
  Project-URL: Documentation, https://docs.polynode.dev
@@ -36,7 +36,11 @@ Description-Content-Type: text/markdown
36
36
 
37
37
  Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
38
38
 
39
- **New in v0.14.0:** Web platforms can take a user from wallet authorization through an exact browser-signed user-owned order without exposing backend credentials. The SDK imports the shared versioned browser bundle into memory, produces a credential-free signing request with a complete order preview, supports one-time multi-worker state, validates canonical signatures and wallet identity, checks BUY collateral before prompting, and submits with exact zero builder attribution.
39
+ **New in v0.15.0:** Polymarket Protocol V2 markets (October 2026). Upgrade before 2026-11-02 if you trade: `order()` looks up each market's protocol and signs Protocol V2 orders for the right exchange automatically, and split, merge and the new redeem work on them too. Nothing changes for current markets or for streaming and REST users. This is not the April 2026 "V2" (CTF Exchange V2 and pUSD) described elsewhere in this README; see [Polymarket Protocol V2 markets](#polymarket-protocol-v2-markets).
40
+
41
+ **In v0.14.1:** Every prepared user-owned order exposes its canonical V2 exchange `order_hash` before submission. This is the exact CLOB/open-order ID and fill `order_hash`; deposit-wallet orders hash the standard inner `Order`, not the POLY_1271 wallet-signing wrapper. Record it before the network attempt so timeouts can be reconciled without risking a duplicate.
42
+
43
+ **In v0.14.0:** Web platforms can take a user from wallet authorization through an exact browser-signed user-owned order without exposing backend credentials. The SDK imports the shared versioned browser bundle into memory, produces a credential-free signing request with a complete order preview, supports one-time multi-worker state, validates canonical signatures and wallet identity, checks BUY collateral before prompting, and submits with exact zero builder attribution.
40
44
 
41
45
  **In v0.13.0:** Trading added explicit `user_owned` execution. Existing builder mode remains the default; opted-in wallets use zero builder attribution, one wallet-ownership authorization, and strict wallet-bound gasless credentials. Builder credentials and nonzero builder codes fail closed in this mode. EOA-controlled Safe and deposit wallets are supported; legacy Magic/proxy wallets are intentionally excluded from the first release.
42
46
 
@@ -70,7 +74,6 @@ with PolyNode(api_key="pn_live_...") as pn:
70
74
  status = pn.status()
71
75
  connections = pn.connections()
72
76
  markets = pn.markets(count=10)
73
- settlements = pn.recent_settlements(count=5)
74
77
  wallet_positions = pn.wallet_positions(
75
78
  address, redeemable=True, condition_id=condition_id
76
79
  )
@@ -125,7 +128,7 @@ asyncio.run(main())
125
128
 
126
129
  ### Complete V3 API
127
130
 
128
- V3 includes wallets, combos, rewards, credits, identities, markets, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 120 current V3 operations through one consistent Python interface.
131
+ V3 includes wallets, combos, rewards, credits, identities, markets, events, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 129 current V3 operations through one consistent Python interface.
129
132
 
130
133
  ```python
131
134
  import asyncio
@@ -133,7 +136,7 @@ from polynode import AsyncPolyNode
133
136
 
134
137
  async def read_v3(address: str):
135
138
  async with AsyncPolyNode(api_key="pn_live_...") as pn:
136
- print(len(pn.v3.operations)) # 120
139
+ print(len(pn.v3.operations)) # 128
137
140
 
138
141
  combo_activity = await pn.v3.execute(
139
142
  "GET /v3/combos/activity",
@@ -144,11 +147,76 @@ async def read_v3(address: str):
144
147
  path_params={"address": address},
145
148
  query={"limit": 100},
146
149
  )
147
- return combo_activity, wallet_rewards
150
+ scopes = await pn.v3.execute(
151
+ "GET /v3/leaderboard/scopes",
152
+ query={"period": "7d", "scope": "category"},
153
+ )
154
+ markets = await pn.v3.execute(
155
+ "GET /v3/markets",
156
+ query={"status": "open", "limit": 100},
157
+ )
158
+ return combo_activity, wallet_rewards, scopes, markets
148
159
 
149
160
  asyncio.run(read_v3("0xabc..."))
150
161
  ```
151
162
 
163
+ Use the typed Combo P&L helper for All Time realized, unrealized, and total
164
+ P&L, or realized P&L over a fixed or custom half-open window:
165
+
166
+ ```python
167
+ from polynode import PolyNodeV3
168
+
169
+ with PolyNodeV3("pn_live_...") as v3:
170
+ all_time = v3.combo_pnl_leaderboard(
171
+ period="all",
172
+ sort_by="total_pnl",
173
+ )
174
+ custom = v3.combo_pnl_leaderboard(
175
+ period="custom",
176
+ after=1_788_134_400,
177
+ before=1_788_220_800,
178
+ sort_by="realized_pnl",
179
+ )
180
+ ```
181
+
182
+ Wallets with incomplete arbitrary transfer basis or without a current mark are
183
+ omitted rather than returned with zero P&L. Inspect `coverage` on every response
184
+ before presenting the ranking.
185
+
186
+ Market and event catalogs use opaque cursor pagination. Reward-market list
187
+ rows include nullable title, slug, image, event, category, and tag fields.
188
+
189
+ Named perps helpers accept the returned opaque cursor:
190
+
191
+ ```python
192
+ from polynode import PolyNodeV3
193
+
194
+ with PolyNodeV3(api_key="pn_live_...") as v3:
195
+ first = v3.perps_trades(
196
+ "BTC-USD",
197
+ after=1_787_875_200,
198
+ before=1_787_961_600,
199
+ limit=100,
200
+ )
201
+ if first.get("next_cursor"):
202
+ second = v3.perps_trades(
203
+ "BTC-USD",
204
+ after=1_787_875_200,
205
+ before=1_787_961_600,
206
+ limit=100,
207
+ cursor=first["next_cursor"],
208
+ )
209
+
210
+ events = v3.perps_events(address=address, limit=100)
211
+ liquidations = v3.perps_liquidations(min_notional=10_000)
212
+ flows = v3.perps_wallet_flows(address, limit=100)
213
+ ```
214
+
215
+ Reuse the same route and filters on every cursor page. Wallet-flow totals and
216
+ counts remain fixed during one traversal. Cumulative volume delta remains a
217
+ bounded single-page series and has no cursor helper. Async clients expose the
218
+ same methods with `await`.
219
+
152
220
  The SDK encodes path parameters for you. Read requests that are safe to repeat retry transient failures and honor `Retry-After`; requests that change data are never retried automatically. JSON decimals decode as `Decimal`, and `ApiError` exposes the status, request ID, retry details, and a request URL with credentials removed.
153
221
 
154
222
  Current presets include `dome`, `fills`, `combos`, `redemptions`, and `deposits`. Current filters include `since()`, `combo_condition_ids()`, `leg_position_ids()`, `event_ids()`, `module_ids()`, `action()`, and `direction()`.
@@ -299,6 +367,43 @@ For the V2 order flow, required approvals, and common failure modes, see [docs.p
299
367
 
300
368
  V2 fees are determined at match time and are not signed into an order, so V2 payloads omit `feeRateBps`, `nonce`, and `taker`. Explicit legacy V1 mode still signs `feeRateBps`; for that path the SDK fetches `/fee-rate` and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.
301
369
 
370
+ #### Polymarket Protocol V2 markets
371
+
372
+ > **Two different "V2"s.** Polymarket Protocol V2 (October 2026) is Polymarket's new position system: positions live in a new ledger (PositionManager), every market type trades on one new exchange (ExchangeV3, order signing domain version `"3"`), and split, merge and redeem go through a Router. It is **not** the April 2026 upgrade (CTF Exchange V2 with pUSD collateral) that `ExchangeVersion.V2` and the rest of this README call "V2". Current markets keep using the April exchanges.
373
+
374
+ **When.** Polymarket's test markets for Protocol V2 trade from 2026-10-05 to 2026-10-30. New markets move to Protocol V2 from around 2026-11-02 (tentative, per Polymarket). Existing markets stay where they are.
375
+
376
+ **What you need to do.**
377
+
378
+ - Streaming and REST only: nothing. Same methods, same response shapes. Rows and events for Protocol V2 markets carry a few optional fields: `protocol_version: "v2"`, `module` (`"binary"` or `"neg_risk"`), `exchange_version: "v3"` on fills, and `payouts_ppm` / `derived` on oracle events. A row without them is a current market.
379
+ - Trading: upgrade to `polynode` 0.15.0 before 2026-11-02 and place orders exactly as before. Older versions keep working on current markets, but Polymarket rejects their orders on Protocol V2 markets (no funds move).
380
+ - Storing ids yourself: keep outcome ids as strings (up to 78 digits). Condition ids are returned in the 66-character form; the 64-character form is accepted as input.
381
+
382
+ The first order on a Protocol V2 market grants the two approvals it needs (pUSD and PositionManager, both for ExchangeV3): one gasless batch for Safe and deposit wallets, or two transactions with a little POL gas for an EOA. To do it ahead of time, or for browser-signed user-owned orders, call `ensure_ready` once with `protocol_v2=True`. Plain `ensure_ready()` behaves exactly as before.
383
+
384
+ ```python
385
+ from polynode.trading import MergeParams, RedeemParams, SplitParams
386
+
387
+ await trader.ensure_ready("0xYourPrivateKey...", protocol_v2=True) # optional
388
+ version = await trader.get_market_protocol(token_id) # "v1" (current) or "v2"
389
+
390
+ # Same call for every market; the SDK picks the exchange and signing domain.
391
+ await trader.order(OrderParams(token_id=token_id, side="BUY", price=0.5, size=10))
392
+
393
+ # Position operations (user-owned execution) route through the Router on V2 markets.
394
+ await trader.execute_split(SplitParams(condition_id=condition_id, amount=10))
395
+ await trader.execute_merge(MergeParams(condition_id=condition_id, amount=10))
396
+ await trader.execute_redeem(RedeemParams(condition_id=condition_id, outcome_index=0)) # 0 = YES, 1 = NO
397
+ payout = await trader.preview_payout(position_id, 10) # None until resolved
398
+
399
+ status = await trader.check_approvals(protocol_v2=True)
400
+ print(status.protocol_v2_ready, status.position_ops_v2_ready, status.auto_redeem_enabled)
401
+ ```
402
+
403
+ The SDK learns a market's protocol from Polymarket's market data and caches it for 24 hours in a new local table (existing tables are unchanged). It never guesses from the shape of an id: if the protocol cannot be learned, the order is refused with a retryable `MarketProtocolError` instead of being signed for the wrong exchange. Pass `protocol_version="v2"` (or call `trader.hint_market_protocol(...)`) to skip the lookup; a value that contradicts what the SDK already knows is refused. Short-form streams pick `positionIds` for Protocol V2 markets and expose `market.protocol_version`, so orders on discovered markets skip the lookup.
404
+
405
+ Results of Protocol V2 markets are shares of 1,000,000 (`[1000000, 0]` = YES won, `[500000, 500000]` = 50/50); `payouts` carries the same result in smallest whole numbers, like current markets. `RedemptionWatcher` handles both. Automatic redemption by Polymarket's AutoRedeemer is opt-in only (`enable_auto_redeem()` / `disable_auto_redeem()`); proceeds always return to your wallet. Converting neg-risk positions is not supported for Protocol V2 markets yet.
406
+
302
407
  #### Optional user-owned execution
303
408
 
304
409
  Set `execution_mode=ExecutionMode.USER_OWNED` when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.
@@ -536,29 +641,82 @@ const result = await fetch("/api/orders/submit", {
536
641
  }).then((response) => response.json());
537
642
  ```
538
643
 
539
- On the backend, atomically take the prepared object from session state, then validate and submit it:
644
+ On the backend, atomically take the prepared object from session state, durably record the deterministic order identity, then validate and submit it:
540
645
 
541
646
  ```python
542
- async def submit_order(trader, browser_result, prepared_orders):
647
+ async def submit_order(trader, browser_result, prepared_orders, attempts):
543
648
  # `take_once` must delete atomically so two workers cannot submit the same request.
544
649
  prepared = await prepared_orders.take_once(browser_result.request_id)
545
650
  if prepared is None:
546
651
  raise ValueError("Unknown or already-used signing request")
547
652
 
548
- result = await trader.submit_prepared_user_owned_order(
549
- prepared,
653
+ order_hash = prepared.order_hash
654
+ # This durable, idempotent write must commit before submission starts.
655
+ await attempts.create_once(
656
+ order_hash=order_hash,
550
657
  request_id=browser_result.request_id,
551
- address=browser_result.address,
552
- signature=browser_result.signature,
658
+ wallet=prepared.signing_request.address,
659
+ token_id=prepared.signing_request.order["tokenId"],
660
+ side=prepared.signing_request.order["side"],
661
+ status="submitting",
553
662
  )
554
- return {
555
- "success": result.success,
556
- "orderId": result.order_id,
557
- "error": result.error,
558
- }
663
+
664
+ try:
665
+ result = await trader.submit_prepared_user_owned_order(
666
+ prepared,
667
+ request_id=browser_result.request_id,
668
+ address=browser_result.address,
669
+ signature=browser_result.signature,
670
+ )
671
+ except BaseException:
672
+ # Once consumed, a local/network exception has an ambiguous outcome.
673
+ status = (
674
+ "outcome_unknown"
675
+ if prepared.consumed
676
+ else "rejected_before_submission"
677
+ )
678
+ await attempts.set_status(order_hash, status)
679
+ raise
680
+ else:
681
+ if result.order_id and result.order_id.lower() != order_hash.lower():
682
+ await attempts.set_status(order_hash, "identity_mismatch")
683
+ raise RuntimeError("CLOB returned an unexpected order ID")
684
+ await attempts.record_response(order_hash, result)
685
+ return {
686
+ "success": result.success,
687
+ "orderId": result.order_id or order_hash,
688
+ "error": result.error,
689
+ }
559
690
  ```
560
691
 
561
- `prepare_user_owned_order()` supports V2 user-owned execution only, expires after five minutes by default, forces zero builder attribution, rejects positive fee authentication, and performs no submission. GTC, FOK, and FAK orders must omit expiration (zero is accepted and canonicalized to no expiration); GTD requires a fresh Unix-seconds expiration with the 60-second safety buffer, and the signing request is clamped to that window. `submit_prepared_user_owned_order()` verifies the request ID, expiry, connected wallet, stored wallet/funder identity, exact typed data, and recovered signer before consuming the request. A consumed request cannot be replayed. If submission has an ambiguous network result, reconcile its status instead of preparing an automatic duplicate.
692
+ `prepared.order_hash` is the lowercase `0x`-prefixed EIP-712 digest of the canonical standard exchange `Order`. For EOA and Safe orders it also matches the browser-signing digest. POLY_1271 browsers sign an outer `TypedDataSign` wrapper, whose digest is intentionally different; `order_hash` always identifies the inner order that the CLOB and fills report. The property is computed on demand, is unchanged by trusted-state export/import, and is not added to the serialized schema.
693
+
694
+ `prepare_user_owned_order()` supports V2 user-owned execution only, expires after five minutes by default, forces zero builder attribution, rejects positive fee authentication, and performs no submission. GTC, FOK, and FAK orders must omit expiration (zero is accepted and canonicalized to no expiration); GTD requires a fresh Unix-seconds expiration with the 60-second safety buffer, and the signing request is clamped to that window. `submit_prepared_user_owned_order()` verifies the request ID, expiry, connected wallet, stored wallet/funder identity, exact typed data, and recovered signer before consuming the request. A consumed request cannot be replayed.
695
+
696
+ Treat any exception after `prepared.consumed` becomes true—including an HTTP timeout, disconnect, cancellation, or local failure around the network call—as `outcome_unknown`. Never submit that hash again and never automatically prepare a replacement. Reconcile by exact hash against both `await trader.get_open_orders(asset_id=token_id)` (`OpenOrder.id`) and your durable authenticated V2 fill source (`fill.order_hash`):
697
+
698
+ ```python
699
+ async def reconcile_unknown(trader, attempt, fills):
700
+ open_orders = await trader.get_open_orders(asset_id=attempt.token_id)
701
+ open_order = next(
702
+ (order for order in open_orders if order.id.lower() == attempt.order_hash.lower()),
703
+ None,
704
+ )
705
+ matching_fills = await fills.by_order_hash(attempt.order_hash)
706
+ return {"openOrder": open_order, "fills": matching_fills}
707
+ ```
708
+
709
+ An open order may already be partially filled (`size_matched` is nonzero), while a fully filled or canceled order will not remain open. Group all partial fills for the exact hash. Finding neither an open order nor a fill is not proof that submission failed: keep the attempt unknown and retry reconciliation with bounded backoff. The application-owned attempt store and fill source must be durable, wallet-scoped, and idempotent by `order_hash`; do not infer identity from token, side, amounts, timestamps, balances, or request ID.
710
+
711
+ If a request-scoped trader was populated from a browser bundle or decrypted vault record, clear its credentials even when validation, signing, submission, or reconciliation fails:
712
+
713
+ ```python
714
+ trader = await trader_from_browser_bundle(request_json)
715
+ try:
716
+ result = await submit_order(trader, browser_result, prepared_orders, attempts)
717
+ finally:
718
+ trader.close()
719
+ ```
562
720
 
563
721
  For a single-process application, a backend-only in-memory dictionary is sufficient. For multiple workers, serialize only with the SDK's trusted server-state methods and use a server-side store that can atomically take/delete by `requestId`:
564
722
 
@@ -2,7 +2,11 @@
2
2
 
3
3
  Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
4
4
 
5
- **New in v0.14.0:** Web platforms can take a user from wallet authorization through an exact browser-signed user-owned order without exposing backend credentials. The SDK imports the shared versioned browser bundle into memory, produces a credential-free signing request with a complete order preview, supports one-time multi-worker state, validates canonical signatures and wallet identity, checks BUY collateral before prompting, and submits with exact zero builder attribution.
5
+ **New in v0.15.0:** Polymarket Protocol V2 markets (October 2026). Upgrade before 2026-11-02 if you trade: `order()` looks up each market's protocol and signs Protocol V2 orders for the right exchange automatically, and split, merge and the new redeem work on them too. Nothing changes for current markets or for streaming and REST users. This is not the April 2026 "V2" (CTF Exchange V2 and pUSD) described elsewhere in this README; see [Polymarket Protocol V2 markets](#polymarket-protocol-v2-markets).
6
+
7
+ **In v0.14.1:** Every prepared user-owned order exposes its canonical V2 exchange `order_hash` before submission. This is the exact CLOB/open-order ID and fill `order_hash`; deposit-wallet orders hash the standard inner `Order`, not the POLY_1271 wallet-signing wrapper. Record it before the network attempt so timeouts can be reconciled without risking a duplicate.
8
+
9
+ **In v0.14.0:** Web platforms can take a user from wallet authorization through an exact browser-signed user-owned order without exposing backend credentials. The SDK imports the shared versioned browser bundle into memory, produces a credential-free signing request with a complete order preview, supports one-time multi-worker state, validates canonical signatures and wallet identity, checks BUY collateral before prompting, and submits with exact zero builder attribution.
6
10
 
7
11
  **In v0.13.0:** Trading added explicit `user_owned` execution. Existing builder mode remains the default; opted-in wallets use zero builder attribution, one wallet-ownership authorization, and strict wallet-bound gasless credentials. Builder credentials and nonzero builder codes fail closed in this mode. EOA-controlled Safe and deposit wallets are supported; legacy Magic/proxy wallets are intentionally excluded from the first release.
8
12
 
@@ -36,7 +40,6 @@ with PolyNode(api_key="pn_live_...") as pn:
36
40
  status = pn.status()
37
41
  connections = pn.connections()
38
42
  markets = pn.markets(count=10)
39
- settlements = pn.recent_settlements(count=5)
40
43
  wallet_positions = pn.wallet_positions(
41
44
  address, redeemable=True, condition_id=condition_id
42
45
  )
@@ -91,7 +94,7 @@ asyncio.run(main())
91
94
 
92
95
  ### Complete V3 API
93
96
 
94
- V3 includes wallets, combos, rewards, credits, identities, markets, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 120 current V3 operations through one consistent Python interface.
97
+ V3 includes wallets, combos, rewards, credits, identities, markets, events, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 129 current V3 operations through one consistent Python interface.
95
98
 
96
99
  ```python
97
100
  import asyncio
@@ -99,7 +102,7 @@ from polynode import AsyncPolyNode
99
102
 
100
103
  async def read_v3(address: str):
101
104
  async with AsyncPolyNode(api_key="pn_live_...") as pn:
102
- print(len(pn.v3.operations)) # 120
105
+ print(len(pn.v3.operations)) # 128
103
106
 
104
107
  combo_activity = await pn.v3.execute(
105
108
  "GET /v3/combos/activity",
@@ -110,11 +113,76 @@ async def read_v3(address: str):
110
113
  path_params={"address": address},
111
114
  query={"limit": 100},
112
115
  )
113
- return combo_activity, wallet_rewards
116
+ scopes = await pn.v3.execute(
117
+ "GET /v3/leaderboard/scopes",
118
+ query={"period": "7d", "scope": "category"},
119
+ )
120
+ markets = await pn.v3.execute(
121
+ "GET /v3/markets",
122
+ query={"status": "open", "limit": 100},
123
+ )
124
+ return combo_activity, wallet_rewards, scopes, markets
114
125
 
115
126
  asyncio.run(read_v3("0xabc..."))
116
127
  ```
117
128
 
129
+ Use the typed Combo P&L helper for All Time realized, unrealized, and total
130
+ P&L, or realized P&L over a fixed or custom half-open window:
131
+
132
+ ```python
133
+ from polynode import PolyNodeV3
134
+
135
+ with PolyNodeV3("pn_live_...") as v3:
136
+ all_time = v3.combo_pnl_leaderboard(
137
+ period="all",
138
+ sort_by="total_pnl",
139
+ )
140
+ custom = v3.combo_pnl_leaderboard(
141
+ period="custom",
142
+ after=1_788_134_400,
143
+ before=1_788_220_800,
144
+ sort_by="realized_pnl",
145
+ )
146
+ ```
147
+
148
+ Wallets with incomplete arbitrary transfer basis or without a current mark are
149
+ omitted rather than returned with zero P&L. Inspect `coverage` on every response
150
+ before presenting the ranking.
151
+
152
+ Market and event catalogs use opaque cursor pagination. Reward-market list
153
+ rows include nullable title, slug, image, event, category, and tag fields.
154
+
155
+ Named perps helpers accept the returned opaque cursor:
156
+
157
+ ```python
158
+ from polynode import PolyNodeV3
159
+
160
+ with PolyNodeV3(api_key="pn_live_...") as v3:
161
+ first = v3.perps_trades(
162
+ "BTC-USD",
163
+ after=1_787_875_200,
164
+ before=1_787_961_600,
165
+ limit=100,
166
+ )
167
+ if first.get("next_cursor"):
168
+ second = v3.perps_trades(
169
+ "BTC-USD",
170
+ after=1_787_875_200,
171
+ before=1_787_961_600,
172
+ limit=100,
173
+ cursor=first["next_cursor"],
174
+ )
175
+
176
+ events = v3.perps_events(address=address, limit=100)
177
+ liquidations = v3.perps_liquidations(min_notional=10_000)
178
+ flows = v3.perps_wallet_flows(address, limit=100)
179
+ ```
180
+
181
+ Reuse the same route and filters on every cursor page. Wallet-flow totals and
182
+ counts remain fixed during one traversal. Cumulative volume delta remains a
183
+ bounded single-page series and has no cursor helper. Async clients expose the
184
+ same methods with `await`.
185
+
118
186
  The SDK encodes path parameters for you. Read requests that are safe to repeat retry transient failures and honor `Retry-After`; requests that change data are never retried automatically. JSON decimals decode as `Decimal`, and `ApiError` exposes the status, request ID, retry details, and a request URL with credentials removed.
119
187
 
120
188
  Current presets include `dome`, `fills`, `combos`, `redemptions`, and `deposits`. Current filters include `since()`, `combo_condition_ids()`, `leg_position_ids()`, `event_ids()`, `module_ids()`, `action()`, and `direction()`.
@@ -265,6 +333,43 @@ For the V2 order flow, required approvals, and common failure modes, see [docs.p
265
333
 
266
334
  V2 fees are determined at match time and are not signed into an order, so V2 payloads omit `feeRateBps`, `nonce`, and `taker`. Explicit legacy V1 mode still signs `feeRateBps`; for that path the SDK fetches `/fee-rate` and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.
267
335
 
336
+ #### Polymarket Protocol V2 markets
337
+
338
+ > **Two different "V2"s.** Polymarket Protocol V2 (October 2026) is Polymarket's new position system: positions live in a new ledger (PositionManager), every market type trades on one new exchange (ExchangeV3, order signing domain version `"3"`), and split, merge and redeem go through a Router. It is **not** the April 2026 upgrade (CTF Exchange V2 with pUSD collateral) that `ExchangeVersion.V2` and the rest of this README call "V2". Current markets keep using the April exchanges.
339
+
340
+ **When.** Polymarket's test markets for Protocol V2 trade from 2026-10-05 to 2026-10-30. New markets move to Protocol V2 from around 2026-11-02 (tentative, per Polymarket). Existing markets stay where they are.
341
+
342
+ **What you need to do.**
343
+
344
+ - Streaming and REST only: nothing. Same methods, same response shapes. Rows and events for Protocol V2 markets carry a few optional fields: `protocol_version: "v2"`, `module` (`"binary"` or `"neg_risk"`), `exchange_version: "v3"` on fills, and `payouts_ppm` / `derived` on oracle events. A row without them is a current market.
345
+ - Trading: upgrade to `polynode` 0.15.0 before 2026-11-02 and place orders exactly as before. Older versions keep working on current markets, but Polymarket rejects their orders on Protocol V2 markets (no funds move).
346
+ - Storing ids yourself: keep outcome ids as strings (up to 78 digits). Condition ids are returned in the 66-character form; the 64-character form is accepted as input.
347
+
348
+ The first order on a Protocol V2 market grants the two approvals it needs (pUSD and PositionManager, both for ExchangeV3): one gasless batch for Safe and deposit wallets, or two transactions with a little POL gas for an EOA. To do it ahead of time, or for browser-signed user-owned orders, call `ensure_ready` once with `protocol_v2=True`. Plain `ensure_ready()` behaves exactly as before.
349
+
350
+ ```python
351
+ from polynode.trading import MergeParams, RedeemParams, SplitParams
352
+
353
+ await trader.ensure_ready("0xYourPrivateKey...", protocol_v2=True) # optional
354
+ version = await trader.get_market_protocol(token_id) # "v1" (current) or "v2"
355
+
356
+ # Same call for every market; the SDK picks the exchange and signing domain.
357
+ await trader.order(OrderParams(token_id=token_id, side="BUY", price=0.5, size=10))
358
+
359
+ # Position operations (user-owned execution) route through the Router on V2 markets.
360
+ await trader.execute_split(SplitParams(condition_id=condition_id, amount=10))
361
+ await trader.execute_merge(MergeParams(condition_id=condition_id, amount=10))
362
+ await trader.execute_redeem(RedeemParams(condition_id=condition_id, outcome_index=0)) # 0 = YES, 1 = NO
363
+ payout = await trader.preview_payout(position_id, 10) # None until resolved
364
+
365
+ status = await trader.check_approvals(protocol_v2=True)
366
+ print(status.protocol_v2_ready, status.position_ops_v2_ready, status.auto_redeem_enabled)
367
+ ```
368
+
369
+ The SDK learns a market's protocol from Polymarket's market data and caches it for 24 hours in a new local table (existing tables are unchanged). It never guesses from the shape of an id: if the protocol cannot be learned, the order is refused with a retryable `MarketProtocolError` instead of being signed for the wrong exchange. Pass `protocol_version="v2"` (or call `trader.hint_market_protocol(...)`) to skip the lookup; a value that contradicts what the SDK already knows is refused. Short-form streams pick `positionIds` for Protocol V2 markets and expose `market.protocol_version`, so orders on discovered markets skip the lookup.
370
+
371
+ Results of Protocol V2 markets are shares of 1,000,000 (`[1000000, 0]` = YES won, `[500000, 500000]` = 50/50); `payouts` carries the same result in smallest whole numbers, like current markets. `RedemptionWatcher` handles both. Automatic redemption by Polymarket's AutoRedeemer is opt-in only (`enable_auto_redeem()` / `disable_auto_redeem()`); proceeds always return to your wallet. Converting neg-risk positions is not supported for Protocol V2 markets yet.
372
+
268
373
  #### Optional user-owned execution
269
374
 
270
375
  Set `execution_mode=ExecutionMode.USER_OWNED` when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.
@@ -502,29 +607,82 @@ const result = await fetch("/api/orders/submit", {
502
607
  }).then((response) => response.json());
503
608
  ```
504
609
 
505
- On the backend, atomically take the prepared object from session state, then validate and submit it:
610
+ On the backend, atomically take the prepared object from session state, durably record the deterministic order identity, then validate and submit it:
506
611
 
507
612
  ```python
508
- async def submit_order(trader, browser_result, prepared_orders):
613
+ async def submit_order(trader, browser_result, prepared_orders, attempts):
509
614
  # `take_once` must delete atomically so two workers cannot submit the same request.
510
615
  prepared = await prepared_orders.take_once(browser_result.request_id)
511
616
  if prepared is None:
512
617
  raise ValueError("Unknown or already-used signing request")
513
618
 
514
- result = await trader.submit_prepared_user_owned_order(
515
- prepared,
619
+ order_hash = prepared.order_hash
620
+ # This durable, idempotent write must commit before submission starts.
621
+ await attempts.create_once(
622
+ order_hash=order_hash,
516
623
  request_id=browser_result.request_id,
517
- address=browser_result.address,
518
- signature=browser_result.signature,
624
+ wallet=prepared.signing_request.address,
625
+ token_id=prepared.signing_request.order["tokenId"],
626
+ side=prepared.signing_request.order["side"],
627
+ status="submitting",
519
628
  )
520
- return {
521
- "success": result.success,
522
- "orderId": result.order_id,
523
- "error": result.error,
524
- }
629
+
630
+ try:
631
+ result = await trader.submit_prepared_user_owned_order(
632
+ prepared,
633
+ request_id=browser_result.request_id,
634
+ address=browser_result.address,
635
+ signature=browser_result.signature,
636
+ )
637
+ except BaseException:
638
+ # Once consumed, a local/network exception has an ambiguous outcome.
639
+ status = (
640
+ "outcome_unknown"
641
+ if prepared.consumed
642
+ else "rejected_before_submission"
643
+ )
644
+ await attempts.set_status(order_hash, status)
645
+ raise
646
+ else:
647
+ if result.order_id and result.order_id.lower() != order_hash.lower():
648
+ await attempts.set_status(order_hash, "identity_mismatch")
649
+ raise RuntimeError("CLOB returned an unexpected order ID")
650
+ await attempts.record_response(order_hash, result)
651
+ return {
652
+ "success": result.success,
653
+ "orderId": result.order_id or order_hash,
654
+ "error": result.error,
655
+ }
525
656
  ```
526
657
 
527
- `prepare_user_owned_order()` supports V2 user-owned execution only, expires after five minutes by default, forces zero builder attribution, rejects positive fee authentication, and performs no submission. GTC, FOK, and FAK orders must omit expiration (zero is accepted and canonicalized to no expiration); GTD requires a fresh Unix-seconds expiration with the 60-second safety buffer, and the signing request is clamped to that window. `submit_prepared_user_owned_order()` verifies the request ID, expiry, connected wallet, stored wallet/funder identity, exact typed data, and recovered signer before consuming the request. A consumed request cannot be replayed. If submission has an ambiguous network result, reconcile its status instead of preparing an automatic duplicate.
658
+ `prepared.order_hash` is the lowercase `0x`-prefixed EIP-712 digest of the canonical standard exchange `Order`. For EOA and Safe orders it also matches the browser-signing digest. POLY_1271 browsers sign an outer `TypedDataSign` wrapper, whose digest is intentionally different; `order_hash` always identifies the inner order that the CLOB and fills report. The property is computed on demand, is unchanged by trusted-state export/import, and is not added to the serialized schema.
659
+
660
+ `prepare_user_owned_order()` supports V2 user-owned execution only, expires after five minutes by default, forces zero builder attribution, rejects positive fee authentication, and performs no submission. GTC, FOK, and FAK orders must omit expiration (zero is accepted and canonicalized to no expiration); GTD requires a fresh Unix-seconds expiration with the 60-second safety buffer, and the signing request is clamped to that window. `submit_prepared_user_owned_order()` verifies the request ID, expiry, connected wallet, stored wallet/funder identity, exact typed data, and recovered signer before consuming the request. A consumed request cannot be replayed.
661
+
662
+ Treat any exception after `prepared.consumed` becomes true—including an HTTP timeout, disconnect, cancellation, or local failure around the network call—as `outcome_unknown`. Never submit that hash again and never automatically prepare a replacement. Reconcile by exact hash against both `await trader.get_open_orders(asset_id=token_id)` (`OpenOrder.id`) and your durable authenticated V2 fill source (`fill.order_hash`):
663
+
664
+ ```python
665
+ async def reconcile_unknown(trader, attempt, fills):
666
+ open_orders = await trader.get_open_orders(asset_id=attempt.token_id)
667
+ open_order = next(
668
+ (order for order in open_orders if order.id.lower() == attempt.order_hash.lower()),
669
+ None,
670
+ )
671
+ matching_fills = await fills.by_order_hash(attempt.order_hash)
672
+ return {"openOrder": open_order, "fills": matching_fills}
673
+ ```
674
+
675
+ An open order may already be partially filled (`size_matched` is nonzero), while a fully filled or canceled order will not remain open. Group all partial fills for the exact hash. Finding neither an open order nor a fill is not proof that submission failed: keep the attempt unknown and retry reconciliation with bounded backoff. The application-owned attempt store and fill source must be durable, wallet-scoped, and idempotent by `order_hash`; do not infer identity from token, side, amounts, timestamps, balances, or request ID.
676
+
677
+ If a request-scoped trader was populated from a browser bundle or decrypted vault record, clear its credentials even when validation, signing, submission, or reconciliation fails:
678
+
679
+ ```python
680
+ trader = await trader_from_browser_bundle(request_json)
681
+ try:
682
+ result = await submit_order(trader, browser_result, prepared_orders, attempts)
683
+ finally:
684
+ trader.close()
685
+ ```
528
686
 
529
687
  For a single-process application, a backend-only in-memory dictionary is sufficient. For multiple workers, serialize only with the SDK's trusted server-state methods and use a server-side store that can atomically take/delete by `requestId`:
530
688
 
@@ -237,7 +237,7 @@
237
237
  "never_drop_silently": true
238
238
  }
239
239
  },
240
- "operation_count": 120,
240
+ "operation_count": 129,
241
241
  "operations": [
242
242
  {
243
243
  "method": "POST",
@@ -371,12 +371,48 @@
371
371
  "domain": "combos",
372
372
  "retry": "safe_read"
373
373
  },
374
+ {
375
+ "method": "GET",
376
+ "path": "/v3/combos/activity/daily",
377
+ "domain": "combos",
378
+ "retry": "safe_read"
379
+ },
380
+ {
381
+ "method": "GET",
382
+ "path": "/v3/combos/leaderboard",
383
+ "domain": "combos",
384
+ "retry": "safe_read"
385
+ },
374
386
  {
375
387
  "method": "GET",
376
388
  "path": "/v3/combos/redemptions",
377
389
  "domain": "combos",
378
390
  "retry": "safe_read"
379
391
  },
392
+ {
393
+ "method": "GET",
394
+ "path": "/v3/markets",
395
+ "domain": "markets",
396
+ "retry": "safe_read"
397
+ },
398
+ {
399
+ "method": "GET",
400
+ "path": "/v3/markets/stats",
401
+ "domain": "markets",
402
+ "retry": "safe_read"
403
+ },
404
+ {
405
+ "method": "GET",
406
+ "path": "/v3/events",
407
+ "domain": "events",
408
+ "retry": "safe_read"
409
+ },
410
+ {
411
+ "method": "GET",
412
+ "path": "/v3/events/stats",
413
+ "domain": "events",
414
+ "retry": "safe_read"
415
+ },
380
416
  {
381
417
  "method": "GET",
382
418
  "path": "/v3/markets/search",
@@ -485,6 +521,12 @@
485
521
  "domain": "global",
486
522
  "retry": "safe_read"
487
523
  },
524
+ {
525
+ "method": "GET",
526
+ "path": "/v3/leaderboard/scopes",
527
+ "domain": "global",
528
+ "retry": "safe_read"
529
+ },
488
530
  {
489
531
  "method": "GET",
490
532
  "path": "/v3/builders",
@@ -497,6 +539,12 @@
497
539
  "domain": "builders",
498
540
  "retry": "safe_read"
499
541
  },
542
+ {
543
+ "method": "GET",
544
+ "path": "/v3/builders/{code}/leaderboard",
545
+ "domain": "builders",
546
+ "retry": "safe_read"
547
+ },
500
548
  {
501
549
  "method": "GET",
502
550
  "path": "/v3/builders/{code}/trades",
@@ -887,6 +935,12 @@
887
935
  "domain": "perps",
888
936
  "retry": "safe_read"
889
937
  },
938
+ {
939
+ "method": "GET",
940
+ "path": "/v3/perps/activity/daily",
941
+ "domain": "perps",
942
+ "retry": "safe_read"
943
+ },
890
944
  {
891
945
  "method": "GET",
892
946
  "path": "/v3/perps/funding-pnl/{instrument}",