# Phoenix Perps read-only integration `PhoenixPerpsService` and `PhoenixPerpsPosition` live in `com.r35157.libs.phoenix.perps`; the REST implementation is in `impl.ref`. They contain no Evelyn/UI types or signing/trading operations. NenjimHub creates one adapter without a portfolio cache as `phoenix.perps.default` and injects it into both Evelyn Perps processes. Each process owns its own wallet list, refresh worker and snapshot. See [Evelyn](Evelyn.md#12-perps-service-og-mission-control) for consumer behavior and [the canonical system overview](../../r35157_docs/projects/evelyn/README.md#evelynperpsservice) for configuration and deployment status. ## REST reads and account model The implementation uses these official endpoints at `https://perp-api.phoenix.trade`: 1. [`GET /v1/trader/state/{authority}?traderPdaIndex=0`](https://docs.phoenix.trade/api/trader/get-trader-state) returns the wallet authority's version-1 snapshot and subaccounts. The [account model](https://docs.phoenix.trade/sdk/accounts) currently uses trader PDA index 0 for user accounts. Subaccount 0 is cross-margin; other returned subaccounts are isolated. Read every returned subaccount and include only nonzero `basePositionLots`. An omitted `positions` field is documented as optional and means empty; null or a wrong type is rejected. 2. [`GET /v1/view/exchange/markets`](https://docs.phoenix.trade/api/exchange/list-markets) supplies each symbol's `baseLotsDecimals`. 3. [`GET /v1/market/{symbol}/mark-price`](https://docs.phoenix.trade/api/exchange/get-mark-price) supplies `markPrice.price` in USD. The API permits an absent mark price; a held position without a valid positive price fails the lookup rather than appearing as zero. An empty trader state needs only the first request. Otherwise metadata is fetched once and each active symbol's price once per wallet lookup, including when that symbol is held in several subaccounts. Request starts are spaced at least one second apart across both environments to avoid bursts of per-market reads. Each HTTP request has a 10-second timeout and uses the Hub's existing HTTP client (no redirects by default). On HTTP 429 the adapter respects `Retry-After` (seconds or HTTP date; 60 seconds if missing/invalid). Further calls fail fast during that shared cooldown, letting Jupiter and other wallet attempts continue; the normal refresh cycle retries later. There are no automatic retries, new API keys or authentication/signing flows. Public reads worked during verification, despite generated API security annotations referring to bearer auth. HTTP failures, including 401/403/404/429, are errors rather than empty portfolios. The shared pacing follows Phoenix's [rate-limit guidance](https://docs.phoenix.trade/sdk/ws-api-best-practices); it does not guarantee a fixed quota. ## Units, precision and identity The official [SDK's `baseLotsToBaseUnits`](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/units.ts) uses `lots / 10 ** baseLotsDecimals`; the exponent may be negative. The adapter uses: ```text quantity = BigDecimal(basePositionLots).scaleByPowerOfTen(-baseLotsDecimals) sizeUsd = abs(quantity) * markPrice.price ``` All lot counts use `BigInteger`; conversion and multiplication use `BigDecimal` without a rounding context. Jackson parses JSON price numbers directly as decimal, without a `double` intermediate. For example, 2,144 SOL lots at `baseLotsDecimals=2` are 21.44 SOL; at a mark price of 104.45 USD, notional is 2,239.408 USD. Entry price, collateral, virtual quote lots and margin are not substitutes for current notional. Rounding to cents occurs only in the existing EMC display formatter. Within each wallet's Phoenix venue, identity is `traderPdaIndex:subaccountIndex:symbol`. Sequence numbers do not affect identity. Rows sort by PDA index, subaccount index and symbol; labels use `SYMBOL-PERP`, with no extra subaccount tree level. Positive quantity means Long and negative means Short. Thus two SOL subaccounts remain two rows even though their labels match. Authority/PDA identity, snapshot version, arrays, symbols, duplicate subaccounts or positions, integer lots, metadata and prices are validated. A malformed lookup fails as a whole; it never publishes a partially decoded portfolio. The shared Jackson mapper is copied so strict parsing does not change other integrations. ## Refresh behavior and limitations Each Evelyn process performs an initial background refresh, then waits 60 seconds following each completed cycle. Jupiter and Phoenix are attempted independently for each configured wallet. Successful results replace only that wallet/venue; failures log wallet and venue and retain its last successful result. A valid empty result clears only that venue's positions, preserving its group. Snapshot reads never fetch external data; EMC's three-second polling remains unchanged. Positions and prices are separate REST reads and can belong to different slots. This is a periodic current-notional estimate, not an atomic exchange valuation. A cycle's duration includes all sequential reads and request pacing before the 60-second pause (32 active markets need at least about 33 seconds for their reads). Failed reads can leave old data visible indefinitely; there is no stale/error indicator yet. Trader PDA indices other than 0, streaming and position management are out of scope.