The Polymarket data-api /positions endpoint (https://data-api.polymarket.com/positions?user=<addr>) reports resolved-but-unredeemed positions in a way that breaks naive PnL aggregation: a resolved loss shows its loss in cashPnl (unrealized) with realizedPnl: 0, so a buy-and-hold-to-resolution strategy looks like “$0 realized, everything unrealized”. The fix is to split realized vs unrealized on the redeemable flag, not on realizedPnl. Drives PM’s /wallets command (crates/position_manager/src/self_portfolio.rs aggregate()).
For Agents
Endpoint:
GET https://data-api.polymarket.com/positions?user=<addr>. Verified live against deposit wallet0xAdAc17C0A3725D2CA2Df66E6DcBBAC82ebA0aEb2on 2026-06-28. Fix landed in commitb26b53b(position-managermain), inaggregate(). Note:/positionsreturns only currently-HELD positions (size > 0), so it cannot see lifetime realized PnL.
The Gotcha — realizedPnl stays 0 until you actually REDEEM
Per-position fields from /positions, by lifecycle state (verified live):
| State | redeemable | curPrice | currentValue | cashPnl | realizedPnl |
|---|---|---|---|---|---|
| Open (unresolved) | false | strictly 0 < p < 1 | mid-value | unrealized P/L | partial-sell realized |
| Resolved LOSS (held, not redeemed) | true | 0 | 0 | -initialValue (the loss, as unrealized) | 0 |
| Resolved WIN (held, not redeemed) | true | 1 | claimable payout | the win | 0 |
A resolved bet's outcome sits in cashPnl (unrealized) until redeemed
realizedPnlonly becomes non-zero when the position is REDEEMED (claimed) or partially sold. A resolved losing bet reportscurrentValue: 0,cashPnl: -initialValue,realizedPnl: 0,redeemable: true,curPrice: 0. So for a strategy that buys and holds to resolution, summing every position’scashPnlinto unrealized produces a misleadinguPnLwhilerPnLstays $0.
Concrete impact (verified, wallet 0xAdAc)
Naive aggregation (sum all cashPnl → unrealized) reported uPnL −0.
The economically-correct split is uPnL −130.89 (18 resolved losses).
The Fix — classify on redeemable, not realizedPnl
In crates/position_manager/src/self_portfolio.rs aggregate() (commit b26b53b):
redeemable == true(resolved) → foldcashPnlintorealized_pnl_usd.redeemable == false(open) →cashPnlintounrealized_pnl_usd, andinitialValueintocost_basis_usd(sopnl_pctreflects only open positions).value_usdkeeps summing allcurrentValue(resolved loss =0, resolved win = claimable payout, open = mid-value).realizedPnlis still added for all positions (captures partial-sell realized that happened before resolution).
"Realized" here means resolved-but-not-yet-redeemed
/positionsonly returns currently-held positions (size > 0). Once a position is redeemed it drops off the endpoint entirely. So therealized_pnl_usdPM computes is really resolved-but-not-yet-redeemed realized — NOT lifetime realized. True lifetime realized PnL would require/activityor/trades.
Related — pUSD/USDC cash balance contract (same file)
The pUSD/USDC cash balance in self_portfolio.rs is read via eth_call balanceOf (6 decimals) against contract 0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB, using POLYGON_RPC_URL from env. Verified to return a sane balance (~$418 for 0xAdAc), confirming this is the correct collateral contract for this deployment — NOT the standard USDC.e 0x2791Bca1.... This matches the pUSD collateral constant in pmv2-polymarket-deposit-wallet-live-trading-solution-2026-06-26.
Related
- position_manager — PM service overview / execution gate
- pmv2-polymarket-deposit-wallet-live-trading-solution-2026-06-26 — deposit-wallet
0xAdAc, pUSD collateral0xC011a7…, Poly1271 flow - polymarket-fetch — upstream Polymarket data-api / CLOB integration