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.
- {polynode-0.14.0 → polynode-0.15.0}/PKG-INFO +176 -18
- {polynode-0.14.0 → polynode-0.15.0}/README.md +175 -17
- {polynode-0.14.0 → polynode-0.15.0}/core-contract-v1.json +55 -1
- polynode-0.15.0/polynode/__init__.py +161 -0
- polynode-0.15.0/polynode/_version.py +1 -0
- polynode-0.15.0/polynode/market_protocol.py +466 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/redemption_watcher.py +66 -8
- {polynode-0.14.0 → polynode-0.15.0}/polynode/short_form.py +35 -2
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/__init__.py +17 -1
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/clob_api.py +12 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/constants.py +16 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/eip712.py +122 -14
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/onboarding.py +186 -1
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/position_management.py +93 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/sqlite_backend.py +37 -1
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/trader.py +487 -7
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/types.py +121 -9
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/__init__.py +1 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/events.py +59 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/perps.py +62 -1
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/short_form.py +3 -0
- polynode-0.15.0/polynode/types/v3.py +263 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/v3.py +469 -3
- {polynode-0.14.0 → polynode-0.15.0}/polynode/v3_operations.py +9 -0
- {polynode-0.14.0 → polynode-0.15.0}/pyproject.toml +1 -1
- polynode-0.14.0/polynode/__init__.py +0 -83
- polynode-0.14.0/polynode/_version.py +0 -1
- {polynode-0.14.0 → polynode-0.15.0}/.gitignore +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/core-fixtures-v1.json +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/cache/__init__.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/client.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/engine.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/errors.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/orderbook.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/orderbook_integrity.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/orderbook_state.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/perps.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/subscription.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/testing.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/cosigner.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/escrow.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/privy.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/relayer.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/signer.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/trading/user_relayer.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/enums.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/orderbook.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/rest.py +0 -0
- {polynode-0.14.0 → polynode-0.15.0}/polynode/types/ws.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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)) #
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
`
|
|
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.
|
|
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
|
|
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)) #
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
`
|
|
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":
|
|
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}",
|