# Create service login challenge Source: https://docs.phoenix.trade/api/auth/create-service-login-challenge /openapi/phoenix-public-api.json post /v1/auth/login/service/challenge Handles `POST /v1/auth/login/service/challenge` via `post.v1.auth.login.service.challenge`. # Create wallet transaction challenge Source: https://docs.phoenix.trade/api/auth/create-wallet-transaction-challenge /openapi/phoenix-public-api.json post /v1/auth/wallet/transaction-challenge Handles `POST /v1/auth/wallet/transaction-challenge` via `post.v1.auth.wallet.transaction_challenge`. # Get JWKS Source: https://docs.phoenix.trade/api/auth/get-jwks /openapi/phoenix-public-api.json get /v1/auth/jwks Handles `GET /v1/auth/jwks` via `get.v1.auth.jwks`. # Get wallet login nonce Source: https://docs.phoenix.trade/api/auth/get-wallet-login-nonce /openapi/phoenix-public-api.json get /v1/auth/nonce Handles `GET /v1/auth/nonce` via `get.v1.auth.nonce`. # Log in with Privy token Source: https://docs.phoenix.trade/api/auth/log-in-with-privy-token /openapi/phoenix-public-api.json post /v1/auth/login/privy Handles `POST /v1/auth/login/privy` via `post.v1.auth.login.privy`. # Log in with service signature Source: https://docs.phoenix.trade/api/auth/log-in-with-service-signature /openapi/phoenix-public-api.json post /v1/auth/login/service Handles `POST /v1/auth/login/service` via `post.v1.auth.login.service`. # Log in with signed wallet transaction Source: https://docs.phoenix.trade/api/auth/log-in-with-signed-wallet-transaction /openapi/phoenix-public-api.json post /v1/auth/login/wallet/transaction Handles `POST /v1/auth/login/wallet/transaction` via `post.v1.auth.login.wallet.transaction`. # Log in with wallet signature Source: https://docs.phoenix.trade/api/auth/log-in-with-wallet-signature /openapi/phoenix-public-api.json post /v1/auth/login/wallet Handles `POST /v1/auth/login/wallet` via `post.v1.auth.login.wallet`. # Log out Source: https://docs.phoenix.trade/api/auth/log-out /openapi/phoenix-public-api.json post /v1/auth/logout Handles `POST /v1/auth/logout` via `post.v1.auth.logout`. # Refresh session Source: https://docs.phoenix.trade/api/auth/refresh-session /openapi/phoenix-public-api.json post /v1/auth/refresh Handles `POST /v1/auth/refresh` via `post.v1.auth.refresh`. # Build register trader instructions Source: https://docs.phoenix.trade/api/exchange/build-register-trader-instructions /openapi/phoenix-public-api.json post /v1/exchange/build-register-ixs Returns the register and delegated-onboarding instructions for the default trader account without a referral code. The caller chooses the transaction fee payer, builds the transaction locally, and signs it with the trader authority and fee payer. # Get candles Source: https://docs.phoenix.trade/api/exchange/get-candles /openapi/phoenix-public-api.json get /v1/candles/{symbol} Handles `GET /v1/candles/{symbol}` via `get.v1.candles.by_symbol`. # Get commodity market calendar Source: https://docs.phoenix.trade/api/exchange/get-commodity-market-calendar /openapi/phoenix-public-api.json get /v1/market/commodity-calendar Handles `GET /v1/market/commodity-calendar` via `get.v1.market.commodity_calendar`. # Get exchange Source: https://docs.phoenix.trade/api/exchange/get-exchange /openapi/phoenix-public-api.json get /v1/view/exchange Handles `GET /v1/view/exchange` via `get.v1.view.exchange`. # Get exchange keys Source: https://docs.phoenix.trade/api/exchange/get-exchange-keys /openapi/phoenix-public-api.json get /v1/view/exchange/keys Handles `GET /v1/view/exchange/keys` via `get.v1.view.exchange.keys`. # Get exchange snapshot Source: https://docs.phoenix.trade/api/exchange/get-exchange-snapshot /openapi/phoenix-public-api.json get /v1/exchange/snapshot Handles `GET /v1/exchange/snapshot` via `get.v1.exchange.snapshot`. # Get exchange status Source: https://docs.phoenix.trade/api/exchange/get-exchange-status /openapi/phoenix-public-api.json get /v1/view/exchange/status Handles `GET /v1/view/exchange/status` via `get.v1.view.exchange.status`. # Get funding overview Source: https://docs.phoenix.trade/api/exchange/get-funding-overview /openapi/phoenix-public-api.json get /v1/funding/overview Handles `GET /v1/funding/overview` via `get.v1.funding.overview`. # Get funding rate history Source: https://docs.phoenix.trade/api/exchange/get-funding-rate-history /openapi/phoenix-public-api.json get /v1/funding/{symbol}/rates Handles `GET /v1/funding/{symbol}/rates` via `get.v1.funding.by_symbol.rates`. # Get mark price Source: https://docs.phoenix.trade/api/exchange/get-mark-price /openapi/phoenix-public-api.json get /v1/market/{symbol}/mark-price Handles `GET /v1/market/{symbol}/mark-price` via `get.v1.market.by_symbol.mark_price`. # Get market Source: https://docs.phoenix.trade/api/exchange/get-market /openapi/phoenix-public-api.json get /v1/view/exchange/market/{symbol} Handles `GET /v1/view/exchange/market/{symbol}` via `get.v1.view.exchange.market.by_symbol`. # Get market calendar Source: https://docs.phoenix.trade/api/exchange/get-market-calendar /openapi/phoenix-public-api.json get /v1/market/{symbol}/market-calendar Handles `GET /v1/market/{symbol}/market-calendar` via `get.v1.market.by_symbol.market_calendar`. # Get market calendar by ID Source: https://docs.phoenix.trade/api/exchange/get-market-calendar-by-id /openapi/phoenix-public-api.json get /v1/market-calendar/{market_calendar_id} Handles `GET /v1/market-calendar/{market_calendar_id}` via `get.v1.market_calendar.by_market_calendar_id`. # Get market fills Source: https://docs.phoenix.trade/api/exchange/get-market-fills /openapi/phoenix-public-api.json get /v1/trades/{symbol}/fills Returns unaggregated fill records for a market, ordered by time descending. Only fills with valid transaction signatures are included. # Get market stats history Source: https://docs.phoenix.trade/api/exchange/get-market-stats-history /openapi/phoenix-public-api.json get /v1/market/{symbol}/stats Handles `GET /v1/market/{symbol}/stats` via `get.v1.market.by_symbol.stats`. # Get next commodity market transition Source: https://docs.phoenix.trade/api/exchange/get-next-commodity-market-transition /openapi/phoenix-public-api.json get /v1/market/next-commodity-market-transition Handles `GET /v1/market/next-commodity-market-transition` via `get.v1.market.next_commodity_market_transition`. # Get next market calendar transition Source: https://docs.phoenix.trade/api/exchange/get-next-market-calendar-transition /openapi/phoenix-public-api.json get /v1/market/{symbol}/next-market-calendar-transition Handles `GET /v1/market/{symbol}/next-market-calendar-transition` via `get.v1.market.by_symbol.next_market_calendar_transition`. # Get orderbook Source: https://docs.phoenix.trade/api/exchange/get-orderbook /openapi/phoenix-public-api.json get /v1/view/orderbook/{symbol} Handles `GET /v1/view/orderbook/{symbol}` via `get.v1.view.orderbook.by_symbol`. # List markets Source: https://docs.phoenix.trade/api/exchange/list-markets /openapi/phoenix-public-api.json get /v1/view/exchange/markets Handles `GET /v1/view/exchange/markets` via `get.v1.view.exchange.markets`. # Submit register trader transaction Source: https://docs.phoenix.trade/api/exchange/submit-register-trader-transaction /openapi/phoenix-public-api.json post /v1/exchange/send-register-ixs Validates a base64-encoded transaction for the default trader account, signs it with the Phoenix onboarder, simulates it to ensure the onboarder pays no lamports, sends it to Phoenix's RPC, and returns the transaction signature. # API Source: https://docs.phoenix.trade/api/index Phoenix REST and WebSocket API endpoints for market data, trader state, and transaction building. ## API Overview Phoenix exposes a public REST API for snapshots and request/response workflows, plus a WebSocket API for live subscriptions. | Surface | URL | | --------- | ------------------------------------ | | REST API | `https://perp-api.phoenix.trade` | | WebSocket | `wss://perp-api.phoenix.trade/v1/ws` | Use REST when you need a point-in-time response, such as exchange configuration, market metadata, trader state, historical fills, or transaction-builder responses. Use WebSocket subscriptions when you need continuous updates, such as orderbooks, trades, candles, trader state, or exchange parameter changes. ## REST API The REST API accepts and returns JSON. Requests with bodies should send `Content-Type: application/json`. The public reference is organized into: * `Auth` — wallet, service, and session authentication. * `Exchange` — exchange, market, candle, and no-referral register-instruction workflows. * `Invite` — invite validation and referral activation, including user-funded referral activation transactions. * `Trader` — trader state, account history, and transaction builders. Some numeric values are string-encoded in responses so clients can preserve full integer precision. Use the schema in the REST API reference for exact field types. ### Authentication and Errors Most exchange, market, and trader read endpoints are public. Routes that require a session use bearer tokens returned by the authentication endpoints; the REST API reference marks those requirements on each operation. Error responses use a JSON object with an `error` string: ```json theme={null} { "error": "error_code_or_message" } ``` ## WebSocket API The WebSocket API uses JSON client messages: ```json theme={null} { "type": "subscribe", "subscription": { "channel": "orderbook", "symbol": "SOL" } } ``` The WebSocket protocol page documents supported channels, request envelopes, confirmation/error messages, and response payloads. # Activate referral (deprecated June 30, 2026) Source: https://docs.phoenix.trade/api/invite/activate-referral-deprecated-june-30-2026 /openapi/phoenix-public-api.json post /v1/referral/activate Deprecated on June 30, 2026. New onboarding should use delegated on-chain permission accounts or `/v1/referral/activate-tx` instead. # Activate referral with user-funded transaction Source: https://docs.phoenix.trade/api/invite/activate-referral-with-user-funded-transaction /openapi/phoenix-public-api.json post /v1/referral/activate-tx Activates a trader with a valid referral code by validating a user-funded transaction, signing it with the referral activation onboarder, and submitting it. The trader authority or another non-Phoenix payer must pay fees and trader-account rent. # Check wallet invite status Source: https://docs.phoenix.trade/api/invite/check-wallet-invite-status /openapi/phoenix-public-api.json get /v1/invite/check/{wallet} Checks whether a wallet address is whitelisted and returns associated information. # Get referral activation permission Source: https://docs.phoenix.trade/api/invite/get-referral-activation-permission /openapi/phoenix-public-api.json get /v1/referral/activation-permission Returns the trader onboarder, risk authority, and permission account needed to build a referral activation transaction. # Validate invite Source: https://docs.phoenix.trade/api/invite/validate-invite /openapi/phoenix-public-api.json post /v1/invite/validate Validates an invite code and whitelists the wallet if the code is valid and unused. # Acknowledge notifications Source: https://docs.phoenix.trade/api/notifications/acknowledge-notifications /openapi/phoenix-public-api.json post /v1/traders/{trader_pubkey}/notifications/ack/notifications Handles `POST /v1/traders/{trader_pubkey}/notifications/ack/notifications` via `post.v1.traders.by_trader_pubkey.notifications.ack.notifications`. # Acknowledge notifications up to timestamp Source: https://docs.phoenix.trade/api/notifications/acknowledge-notifications-up-to-timestamp /openapi/phoenix-public-api.json post /v1/traders/{trader_pubkey}/notifications/ack/up-to Handles `POST /v1/traders/{trader_pubkey}/notifications/ack/up-to` via `post.v1.traders.by_trader_pubkey.notifications.ack.up_to`. # List trader notifications Source: https://docs.phoenix.trade/api/notifications/list-trader-notifications /openapi/phoenix-public-api.json get /v1/traders/{trader_pubkey}/notifications Handles `GET /v1/traders/{trader_pubkey}/notifications` via `get.v1.traders.by_trader_pubkey.notifications`. # Build attached conditional order transaction Source: https://docs.phoenix.trade/api/trader/build-attached-conditional-order-transaction /openapi/phoenix-public-api.json post /v1/ix/place-attached-conditional-order Handles `POST /v1/ix/place-attached-conditional-order` via `post.v1.ix.place_attached_conditional_order`. # Build cancel conditional order transaction Source: https://docs.phoenix.trade/api/trader/build-cancel-conditional-order-transaction /openapi/phoenix-public-api.json post /v1/ix/cancel-conditional-order Handles `POST /v1/ix/cancel-conditional-order` via `post.v1.ix.cancel_conditional_order`. # Build cancel stop loss order transaction Source: https://docs.phoenix.trade/api/trader/build-cancel-stop-loss-order-transaction /openapi/phoenix-public-api.json post /v1/ix/cancel-stop-loss-order Handles `POST /v1/ix/cancel-stop-loss-order` via `post.v1.ix.cancel_stop_loss_order`. # Build isolated limit order transaction Source: https://docs.phoenix.trade/api/trader/build-isolated-limit-order-transaction /openapi/phoenix-public-api.json post /v1/ix/place-isolated-limit-order Handles `POST /v1/ix/place-isolated-limit-order` via `post.v1.ix.place_isolated_limit_order`. # Build isolated limit order transaction with conditionals Source: https://docs.phoenix.trade/api/trader/build-isolated-limit-order-transaction-with-conditionals /openapi/phoenix-public-api.json post /v1/ix/place-isolated-limit-order-with-conditionals Handles `POST /v1/ix/place-isolated-limit-order-with-conditionals` via `post.v1.ix.place_isolated_limit_order_with_conditionals`. # Build isolated limit order transaction with liquidation estimate Source: https://docs.phoenix.trade/api/trader/build-isolated-limit-order-transaction-with-liquidation-estimate /openapi/phoenix-public-api.json post /v1/ix/place-isolated-limit-order-enhanced Handles `POST /v1/ix/place-isolated-limit-order-enhanced` via `post.v1.ix.place_isolated_limit_order_enhanced`. # Build isolated market order transaction Source: https://docs.phoenix.trade/api/trader/build-isolated-market-order-transaction /openapi/phoenix-public-api.json post /v1/ix/place-isolated-market-order Handles `POST /v1/ix/place-isolated-market-order` via `post.v1.ix.place_isolated_market_order`. # Build isolated market order transaction with liquidation estimate Source: https://docs.phoenix.trade/api/trader/build-isolated-market-order-transaction-with-liquidation-estimate /openapi/phoenix-public-api.json post /v1/ix/place-isolated-market-order-enhanced Handles `POST /v1/ix/place-isolated-market-order-enhanced` via `post.v1.ix.place_isolated_market_order_enhanced`. # Build position conditional order transaction Source: https://docs.phoenix.trade/api/trader/build-position-conditional-order-transaction /openapi/phoenix-public-api.json post /v1/ix/place-position-conditional-order Handles `POST /v1/ix/place-position-conditional-order` via `post.v1.ix.place_position_conditional_order`. # Build stop loss order transaction Source: https://docs.phoenix.trade/api/trader/build-stop-loss-order-transaction /openapi/phoenix-public-api.json post /v1/ix/place-stop-loss-order Handles `POST /v1/ix/place-stop-loss-order` via `post.v1.ix.place_stop_loss_order`. # Get order history Source: https://docs.phoenix.trade/api/trader/get-order-history /openapi/phoenix-public-api.json get /v1/trader/{authority}/order-history Handles `GET /v1/trader/{authority}/order-history` via `get.v1.trader.by_authority.order_history`. # Get order history v2 Source: https://docs.phoenix.trade/api/trader/get-order-history-v2 /openapi/phoenix-public-api.json get /v1/traders/{trader_pubkey}/orders_v2 Handles `GET /v1/traders/{trader_pubkey}/orders_v2` via `get.v1.traders.by_trader_pubkey.orders_v2`. # Get trade history Source: https://docs.phoenix.trade/api/trader/get-trade-history /openapi/phoenix-public-api.json get /v1/trader/{authority}/trades-history Handles `GET /v1/trader/{authority}/trades-history` via `get.v1.trader.by_authority.trades_history`. # Get trader Source: https://docs.phoenix.trade/api/trader/get-trader /openapi/phoenix-public-api.json get /v1/view/trader/{pubkey} Handles `GET /v1/view/trader/{pubkey}` via `get.v1.view.trader.by_pubkey`. # Get trader capabilities Source: https://docs.phoenix.trade/api/trader/get-trader-capabilities /openapi/phoenix-public-api.json get /v1/view/trader-capabilities Handles `GET /v1/view/trader-capabilities` via `get.v1.view.trader_capabilities`. # Get trader collateral history Source: https://docs.phoenix.trade/api/trader/get-trader-collateral-history /openapi/phoenix-public-api.json get /v1/trader/{authority}/collateral-history Handles `GET /v1/trader/{authority}/collateral-history` via `get.v1.trader.by_authority.collateral_history`. # Get trader collateral history Source: https://docs.phoenix.trade/api/trader/get-trader-collateral-history-1 /openapi/phoenix-public-api.json get /v1/traders/{trader_pubkey}/collateral-history Handles `GET /v1/traders/{trader_pubkey}/collateral-history` via `get.v1.traders.by_trader_pubkey.collateral_history`. # Get trader funding history Source: https://docs.phoenix.trade/api/trader/get-trader-funding-history /openapi/phoenix-public-api.json get /v1/trader/{authority}/funding-history Handles `GET /v1/trader/{authority}/funding-history` via `get.v1.trader.by_authority.funding_history`. # Get trader market PnL Source: https://docs.phoenix.trade/api/trader/get-trader-market-pnl /openapi/phoenix-public-api.json get /v1/traders/{trader_pubkey}/pnl/markets Handles `GET /v1/traders/{trader_pubkey}/pnl/markets` via `get.v1.traders.by_trader_pubkey.pnl.markets`. # Get trader PnL Source: https://docs.phoenix.trade/api/trader/get-trader-pnl /openapi/phoenix-public-api.json get /v1/traders/{trader_pubkey}/pnl Handles `GET /v1/traders/{trader_pubkey}/pnl` via `get.v1.traders.by_trader_pubkey.pnl`. # Get trader portfolio values Source: https://docs.phoenix.trade/api/trader/get-trader-portfolio-values /openapi/phoenix-public-api.json get /v1/traders/{trader_pubkey}/portfolio-values Handles `GET /v1/traders/{trader_pubkey}/portfolio-values` via `get.v1.traders.by_trader_pubkey.portfolio_values`. # Get trader state Source: https://docs.phoenix.trade/api/trader/get-trader-state /openapi/phoenix-public-api.json get /v1/trader/state/{authority_pubkey} Handles `GET /v1/trader/state/{authority_pubkey}` via `get.v1.trader.state.by_authority_pubkey`. # Get trader trade history v2 Source: https://docs.phoenix.trade/api/trader/get-trader-trade-history-v2 /openapi/phoenix-public-api.json get /v1/traders/{trader_pubkey}/trades_v2 Handles `GET /v1/traders/{trader_pubkey}/trades_v2` via `get.v1.traders.by_trader_pubkey.trades_v2`. # Get user collateral history Source: https://docs.phoenix.trade/api/trader/get-user-collateral-history /openapi/phoenix-public-api.json get /v1/users/{user_pubkey}/collateral-history Handles `GET /v1/users/{user_pubkey}/collateral-history` via `get.v1.users.by_user_pubkey.collateral_history`. # Get user hourly funding Source: https://docs.phoenix.trade/api/trader/get-user-hourly-funding /openapi/phoenix-public-api.json get /v1/users/{user_pubkey}/funding-hourly Handles `GET /v1/users/{user_pubkey}/funding-hourly` via `get.v1.users.by_user_pubkey.funding_hourly`. # Get user liquidation history Source: https://docs.phoenix.trade/api/trader/get-user-liquidation-history /openapi/phoenix-public-api.json get /v1/users/{user_pubkey}/liquidation-history Handles `GET /v1/users/{user_pubkey}/liquidation-history` via `get.v1.users.by_user_pubkey.liquidation_history`. # Get user PnL Source: https://docs.phoenix.trade/api/trader/get-user-pnl /openapi/phoenix-public-api.json get /v1/users/{user_pubkey}/pnl Handles `GET /v1/users/{user_pubkey}/pnl` via `get.v1.users.by_user_pubkey.pnl`. # Get user trade history v2 Source: https://docs.phoenix.trade/api/trader/get-user-trade-history-v2 /openapi/phoenix-public-api.json get /v1/users/{user_pubkey}/trades_v2 Handles `GET /v1/users/{user_pubkey}/trades_v2` via `get.v1.users.by_user_pubkey.trades_v2`. # WebSocket Source: https://docs.phoenix.trade/api/websocket Supported Phoenix WebSocket subscription requests and response message types. ## Connection Connect to the Phoenix WebSocket endpoint: ```text theme={null} wss://perp-api.phoenix.trade/v1/ws ``` The Rust SDK uses `PHOENIX_WS_URL` when it is set. Otherwise, it derives the WebSocket URL from `PHOENIX_API_URL` by switching `http` to `ws` and using `/v1/ws`. Messages are UTF-8 JSON objects. ## Client messages Subscribe with a `type` and `subscription` envelope: ```json theme={null} { "type": "subscribe", "subscription": { "channel": "orderbook", "symbol": "SOL" } } ``` Unsubscribe with the same subscription object: ```json theme={null} { "type": "unsubscribe", "subscription": { "channel": "orderbook", "symbol": "SOL" } } ``` ## Supported channels | Channel | Subscription fields | Response type | | ------------- | ---------------------------------------- | -------------------------- | | `allMids` | none | `AllMidsData` | | `exchange` | optional `encoding` | `ExchangeMessage` | | `fundingRate` | `symbol` | `FundingRateMessage` | | `orderbook` | `symbol`, optional `bypassExecutionBand` | `L2BookUpdate` | | `traderState` | `authority`, `traderPdaIndex` | `TraderStateServerMessage` | | `market` | `symbol` | `MarketStatsUpdate` | | `trades` | `symbol` | `TradesMessage` | | `candles` | `symbol`, `timeframe` | `CandleData` | ## Control responses The server confirms successful subscriptions with a `subscriptionConfirmed` message: ```json theme={null} { "type": "subscriptionConfirmed", "subscription": { "channel": "orderbook", "symbol": "SOL" } } ``` Subscription-level errors use `subscriptionError`: ```json theme={null} { "type": "subscriptionError", "subscription": { "channel": "orderbook", "symbol": "SOL" }, "code": "invalid_subscription", "message": "invalid subscription" } ``` Server errors use the `error` channel: ```json theme={null} { "channel": "error", "code": 400, "error": "unknown channel" } ``` ## Market subscriptions ### Exchange Use the `exchange` channel to keep local exchange and market-parameter metadata in sync. The server sends an initial snapshot followed by ordered deltas when exchange keys, exchange status, markets, or market parameters change. ```json theme={null} { "type": "subscribe", "subscription": { "channel": "exchange", "encoding": "json" } } ``` The `encoding` field is optional. Omit it to use the default compressed snapshot response: ```json theme={null} { "channel": "exchange", "messageType": "encodedSnapshot", "version": 1, "sequenceNumber": "123", "slot": 123456789, "slotIndex": 0, "reason": "snapshot", "encoding": "base64+zstd", "payload": "..." } ``` Set `encoding` to `json` to receive the initial snapshot as JSON. The examples below are abbreviated to show the fields relevant to cache synchronization: ```json theme={null} { "channel": "exchange", "messageType": "snapshot", "version": 1, "sequenceNumber": "123", "slot": 123456789, "slotIndex": 0, "reason": "snapshot", "exchange": { "programId": "program-pubkey", "globalConfig": "global-config-pubkey", "active": true, "gated": false }, "markets": [ { "symbol": "SOL", "assetId": 1, "marketStatus": "active", "marketPubkey": "market-pubkey", "splinePubkey": "spline-pubkey" } ] } ``` After the snapshot, apply each `delta` with the next `sequenceNumber` to your local cache. If a sequence number is skipped, resubscribe and rebuild from a fresh snapshot. ```json theme={null} { "channel": "exchange", "messageType": "delta", "version": 1, "sequenceNumber": "124", "slot": 123456790, "slotIndex": 0, "ops": [ { "kind": "marketAdded", "market": { "symbol": "ETH", "assetId": 2, "marketStatus": "active", "marketPubkey": "market-pubkey", "splinePubkey": "spline-pubkey" } }, { "kind": "marketClosed", "symbol": "SOL", "previous_market_status": "active", "finalized_mark_price": "150000000" } ] } ``` Handle exchange deltas as cache mutations: | Delta kind | Sync behavior | | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `marketAdded` | Add or replace the market config in the local market map. | | `marketStatusChanged` | Update the market's `marketStatus`. | | `marketClosed` | Mark the market as `closed` and retain the finalized mark price if your client needs settlement context. | | `marketTombstoned` | Mark the market as `tombstoned` and stop treating it as tradeable. | | `marketDeleted` | Remove the market from the local market map. | | `marketParameterUpdated` | Patch the market's risk, funding, fee, cap, or commodity metadata fields from the update payload. | | `exchangeStatusChanged` | Update exchange-level `active`, `gated`, and status feature flags. | | `exchangeKeysUpdated` | Replace exchange-level account and authority keys. | ### All mids ```json theme={null} { "type": "subscribe", "subscription": { "channel": "allMids" } } ``` ```json theme={null} { "channel": "allMids", "mids": { "SOL": 150.25, "BTC": 65000.5 }, "slot": 123456789, "slotIndex": 0 } ``` ### Funding rate ```json theme={null} { "type": "subscribe", "subscription": { "channel": "fundingRate", "symbol": "SOL" } } ``` ```json theme={null} { "channel": "fundingRate", "symbol": "SOL", "funding": 0.0000125 } ``` ### Orderbook Set `bypassExecutionBand` to `true` to receive the full orderbook during commodities after-hours, including price levels outside the tradeable execution band. ```json theme={null} { "type": "subscribe", "subscription": { "channel": "orderbook", "symbol": "SOL", "bypassExecutionBand": false } } ``` ```json theme={null} { "channel": "orderbook", "symbol": "SOL", "orderbook": { "bids": [[150.25, 100.0], [150.2, 200.0]], "asks": [[150.3, 150.0], [150.35, 250.0]], "mid": 150.275 }, "bypassExecutionBand": false } ``` ### Market stats ```json theme={null} { "type": "subscribe", "subscription": { "channel": "market", "symbol": "SOL" } } ``` ```json theme={null} { "channel": "market", "symbol": "SOL", "openInterest": 1000.0, "markPx": 150.1, "midPx": 150.12, "oraclePx": 150.0, "prevDayPx": 148.7, "dayNtlVlm": 2500000.0, "funding": 0.0000125 } ``` ### Trades ```json theme={null} { "type": "subscribe", "subscription": { "channel": "trades", "symbol": "SOL" } } ``` ```json theme={null} { "channel": "trades", "symbol": "SOL", "trades": [ { "slot": "123456789", "slotIndex": 5, "timestamp": "1775578550", "symbol": "SOL", "taker": "taker-pubkey", "tradeSequenceNumber": "100", "side": "bid", "baseLotsFilled": "1000", "quoteLotsFilled": "150000", "feeInQuoteLots": "30", "baseAmount": 10.0, "quoteAmount": 1500.0, "numFills": 2 } ] } ``` ### Candles Supported timeframes are `1s`, `5s`, `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, and `1d`. ```json theme={null} { "type": "subscribe", "subscription": { "channel": "candles", "symbol": "SOL", "timeframe": "1m" } } ``` The server sends candle updates with `channel` set to `candle`: ```json theme={null} { "channel": "candle", "symbol": "SOL", "timeframe": "1m", "candle": { "time": 1727181985, "low": 149.8, "high": 151.5, "open": 150.25, "close": 150.9, "volume": 1234.56, "tradeCount": 89 } } ``` ## Trader subscriptions ### Trader state ```json theme={null} { "type": "subscribe", "subscription": { "channel": "traderState", "authority": "wallet-pubkey", "traderPdaIndex": 0 } } ``` Trader state messages are either `snapshot` or `delta` payloads. ```json theme={null} { "channel": "traderState", "authority": "wallet-pubkey", "traderPdaIndex": 0, "slot": 123456789, "messageType": "snapshot", "version": 1, "capabilities": { "flags": 63, "state": "active", "capabilities": { "placeLimitOrder": { "immediate": true }, "placeMarketOrder": { "immediate": true }, "riskIncreasingTrade": { "immediate": true }, "riskReducingTrade": { "immediate": true }, "depositCollateral": { "immediate": true }, "withdrawCollateral": { "immediate": true } } }, "makerFeeOverrideMultiplier": 1.0, "takerFeeOverrideMultiplier": 1.0, "subaccounts": [] } ``` ```json theme={null} { "channel": "traderState", "authority": "wallet-pubkey", "traderPdaIndex": 0, "slot": 123456790, "messageType": "delta", "deltas": [ { "subaccountIndex": 0, "sequence": 42, "collateral": "1000000", "positions": [], "orders": [], "splines": [], "tradeHistory": [], "orderHistory": [] } ] } ``` # Overview Source: https://docs.phoenix.trade/builder-codes How Flight codes work on Phoenix > Builder codes for Phoenix — earn a share of fees on the order flow you route. Flight is Phoenix's builder-code layer. If you run a trading app, terminal, bot, or agent that sends orders to Phoenix, you can register as a builder, route orders through Flight, and collect a fee on the volume you bring. ## How it works 1. Register a builder at [flight dashboard](https://flight.phoenix.trade). Registration is an on-chain instruction that ties your wallet (the builder authority) to a Phoenix trader account where Flight fees accrue. 2. Set your builder fee in basis points at registration. The fee is added on top of Phoenix's base exchange fees and is paid by the trader on each routed order. 3. Route orders through Flight from your client. Once the SDK client is configured with your builder authority, supported order instructions are wrapped automatically. See the [SDK docs](/sdk/rise) for installation and client setup. 4. Collect fees as collateral on your builder trader account. Withdraw any time from the Flight portal. ## Fees Flight currently collects builder fees on liquidity-removing fills: * Market orders * The taking portion of a limit order that crosses the book If a routed limit order rests as maker liquidity, the resting portion does not generate a Flight fee today. Maker-side fee collection is on the roadmap so builders can also earn on routed liquidity that rests first and fills later. The builder fee is separate from — and stacks on top of — Phoenix's [base taker and maker fees](/phoenix/matching-engine/fees). Builders set their own bps at registration. ## Registering Register using the [flight dashboard](https://flight.phoenix.trade). The portal is also where you track accrued fees and withdraw funds. We recommend registering with a fresh, empty wallet. Builder fees accrue as collateral on the Phoenix trader account tied to your builder authority, so a clean wallet keeps builder revenue isolated from any existing trading collateral. ## Using Flight We recommend creating a **dedicated embedded wallet** for each user who interacts with Phoenix. This keeps their Phoenix activity isolated from other platforms that integrate with Phoenix. ## Onboarding users to Phoenix Flight builders and other integrations can onboard users without referral codes through `POST /v1/exchange/build-register-ixs` followed by `POST /v1/exchange/send-register-ixs`. Your app builds and signs the transaction locally with the user's trader authority and a non-Phoenix fee payer, then the API validates, signs with the Phoenix onboarder, simulates to ensure Phoenix pays no lamports, sends the transaction, and returns the signature. See [Trader Onboarding](/sdk/register#without-a-referral-code). ## SDK integration Rise, the Phoenix SDK, supports Flight-routed order instructions in TypeScript and Rust. See the [SDK docs](/sdk/rise) to install the SDK and create a client. > Flight support in Rise is currently in beta. ## See also * [Fees](/phoenix/matching-engine/fees) * [Referral Program](/incentive-programs/referral-program) * [SDK docs](/sdk/rise) * [FAQ](/phoenix/faq/faq) # Command reference Source: https://docs.phoenix.trade/cli/commands Every Vulcan CLI command, grouped by topic. This reference lists every command group Vulcan exposes. For strategy runners (TWAP and grid), see the dedicated [Strategies](/cli/strategies) page. ## Global flags These flags are available on every command: | Flag | Description | | ----------------------- | -------------------------------------------------------- | | `-o, --output ` | Output format: `table` (default) or `json`. | | `--dry-run` | Simulate the operation without submitting a transaction. | | `-y, --yes` | Skip confirmation prompts. | | `-w, --wallet ` | Stored wallet to use instead of the default. | | `--rpc-url ` | Solana RPC endpoint override. | | `--api-url ` | Phoenix API endpoint override. | | `-v, --verbose` | Enable verbose / debug logging to stderr. | | `--watch` | Watch for live updates via WebSocket where supported. | ## `wallet` — wallet management | Subcommand | Description | | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `create --name ` | Generate a new Solana keypair, encrypt it, and store it. | | `import --name [--format base58\|bytes\|file] ` | Import from base58 string, byte array, or Solana CLI JSON file. | | `list` | List all stored wallets. | | `show ` | Show wallet details (pubkey, default status). | | `set-default ` | Set a wallet as the default for all commands. | | `remove ` | Remove a wallet from local storage. | | `export [--file \| --stdout \| --private-key [--private-key-format base58\|bytes]] [--force]` | Export the encrypted wallet file to `` (`--file`), print the encrypted backup to stdout (`--stdout`), or print the plaintext private key (`--private-key`, requires `--yes`). `--private-key-format` defaults to `base58`; pass `bytes` for the Solana CLI JSON array format. `--force` overwrites an existing `--file` path. | | `balance []` | Show SOL and USDC balances. Defaults to the default wallet. | ```bash theme={null} vulcan wallet create --name my-wallet vulcan wallet balance my-wallet -o json ``` ## `market` — market data | Subcommand | Description | | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `list` | List all available perpetual markets. | | `info ` | Detailed market configuration (tick size, lot size, fees, leverage tiers). | | `ticker ` | Current price, 24h volume, open interest, funding rate. | | `orderbook [--depth ]` | L2 orderbook snapshot. Default depth 10. | | `candles [--interval ] [--limit ] [--with-indicators ]` | OHLCV candles. Defaults: interval `1h`, limit `20`. `--with-indicators` appends derived columns; supported: `sma`, `ema`, `rsi`, `macd`, `bbands`, `atr`, `vwap`, `adx`, `stoch`. | | `trades [--limit ]` | Recent trades. Default limit 20. | | `funding-rates [--limit ]` | Historical funding rates. Default limit 20. | ```bash theme={null} vulcan market ticker SOL -o json vulcan market orderbook SOL --depth 5 ``` ## `trade` — order management | Subcommand | Description | | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | `market-buy [ \| --tokens \| --notional-usdc ] [--tp] [--sl] [--isolated [--collateral ]] [--reduce-only]` | Place a market buy. Provide exactly one of positional size (base lots), `--tokens`, or `--notional-usdc`. | | `market-sell [ \| --tokens \| --notional-usdc ] [--tp] [--sl] [--isolated [--collateral ]] [--reduce-only]` | Place a market sell. Same sizing options as `market-buy`. | | `limit-buy [--tp] [--sl] [--isolated [--collateral ]] [--reduce-only]` | Place a limit buy. Size in base lots. | | `limit-sell [--tp] [--sl] [--isolated [--collateral ]] [--reduce-only]` | Place a limit sell. Size in base lots. | | `cancel ` | Cancel specific orders by ID. | | `cancel-all []` | Cancel all open orders. Omit symbol to cancel across every market. | | `orders []` | List open orders. Omit symbol to list all. | | `set-tpsl [--tp \| --tp-level PRICE[:SIZE_TOKENS] ...] [--sl \| --sl-level PRICE[:SIZE_TOKENS] ...]` | Set take-profit or stop-loss on an existing position. Supports laddered exits. | | `cancel-tpsl [--tp] [--sl]` | Cancel take-profit and/or stop-loss for a position. | ```bash theme={null} vulcan trade market-buy SOL --notional-usdc 100 --tp 250 --sl 180 vulcan trade limit-sell SOL 1.5 230 --reduce-only vulcan trade cancel-all ``` ## `position` — position management | Subcommand | Description | | ---------------------------------------------- | ------------------------------------------------------------ | | `list` | List all open positions. | | `show ` | Detailed view of a specific position. | | `close ` | Close an entire position. | | `close-all` | Close every open position across all markets. | | `reduce ` | Reduce a position by `size` base lots. | | `tp-sl [--tp ] [--sl ]` | Attach take-profit and/or stop-loss to an existing position. | ```bash theme={null} vulcan position list -o json vulcan position close SOL ``` ## `margin` — collateral management | Subcommand | Description | | ------------------------------------------- | ------------------------------------------------------------------------ | | `status` | Show cross-margin health, equity, maintenance margin, available balance. | | `deposit ` | Deposit USDC collateral. | | `withdraw ` | Withdraw USDC collateral. | | `transfer --from --to ` | Transfer collateral between subaccounts (0 = cross). | | `transfer-child-to-parent --child ` | Sweep all collateral from a child subaccount back to cross-margin. | | `sync-parent-to-child --child ` | Sync parent state to a child subaccount. | | `leverage-tiers ` | Show the leverage tier schedule for a market. | | `add-collateral ` | Add USDC to an isolated position. | ```bash theme={null} vulcan margin status -o json vulcan margin deposit 500 ``` ## `account` — trader account | Subcommand | Description | | --------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `register --access-code \| --referral-code \| --invite-code ` | Register a trader account. Exactly one code is required. | | `info` | Show trader account details (PDA, subaccounts, margin mode). | | `subaccounts` | List all subaccounts. | | `create-subaccount [--pda-index ] --subaccount-index ` | Create a new subaccount. | ```bash theme={null} vulcan account register --access-code MY_CODE vulcan account info -o json ``` ## `auth` — Phoenix API authentication | Subcommand | Description | | ---------- | -------------------------------------------------------- | | `login` | Log in to the Phoenix API by signing a wallet challenge. | | `status` | Show redacted Phoenix API session status. | | `logout` | Clear the stored Phoenix API session. | ```bash theme={null} vulcan auth login vulcan auth status -o json ``` ## `portfolio` — portfolio snapshot A single command (no subcommands) that returns margin, positions, and open orders in one call. | Flag | Description | | ---------------------- | --------------------------------------------------------------------------- | | `--include ` | Comma-separated subset of `margin`, `positions`, `orders`. Defaults to all. | ```bash theme={null} vulcan portfolio -o json vulcan portfolio --include margin,positions ``` ## `paper` — local paper trading Paper trading runs against live Phoenix prices but never touches your wallet or on-chain state. | Subcommand | Description | | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `init [--balance ] [--currency ] [--fee-bps ]` | Initialize or overwrite the local paper account. | | `reset [--balance ] [--currency ] [--fee-bps ]` | Reset local paper state. | | `status` | Show paper account status. | | `positions` | Show paper positions. | | `orders` | Show open paper orders. | | `fills [--limit ]` | Show recent paper fills. Default limit 50. | | `buy [--type market\|limit] [--size \| --tokens \| --notional-usdc ] [--price

]` | Place a paper buy order. | | `sell [--type market\|limit] [--size \| --tokens \| --notional-usdc ] [--price

]` | Place a paper sell order. | | `cancel ` | Cancel a paper order. | | `cancel-all []` | Cancel all paper orders, optionally filtered by symbol. | | `reconcile []` | Reconcile resting paper limit orders against live market prices. | ```bash theme={null} vulcan paper init --balance 10000 vulcan paper buy SOL --notional-usdc 100 --type market -o json ``` ## `history` — trade and account history | Subcommand | Description | | ----------------------------------------------------- | ------------------------------------------------- | | `trades [--symbol ] [--limit ] [--cursor ]` | Past trade and fill history. Default limit 20. | | `orders [--symbol ] [--limit ] [--cursor ]` | Past order history. Default limit 20. | | `collateral [--limit ] [--cursor ]` | Deposit and withdrawal history. Default limit 20. | | `funding [--symbol ] [--limit ] [--cursor ]` | Funding payment history. Default limit 20. | | `pnl [--resolution hourly\|daily] [--limit ]` | PnL over time. Defaults to `hourly`, limit 24. | ```bash theme={null} vulcan history trades --symbol SOL --limit 50 vulcan history pnl --resolution daily --limit 30 ``` ## `agent` — agent setup | Subcommand | Description | | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `install [--target ] [--scope user\|project] [--dir ] [--force]` | Install Vulcan agent skills for a client. Targets: `claude`, `cursor`, `codex`, `agentskills`. | | `doctor [--target ] [--scope user\|project] [--dir ]` | Inspect known agent skill locations. | | `health [--target ] [--scope user\|project] [--dir ]` | Combined health check for first-run agent guidance. | | `live-ready [--target ] [--scope user\|project]` | Check whether live agent execution is ready for a target client. | | `mcp print-config [--target ] [--dangerous] [--groups ]` | Print an MCP config snippet for an agent client. | | `mcp doctor [--target ] [--scope ] [--path ]` | Inspect expected MCP config for an agent client. | | `mcp install [--target ] [--scope ] [--path ] [--dangerous] [--groups ] [--force \| --repair]` | Install or update the Vulcan MCP config. `--repair` updates only `command` and `args`, preserving the env (wallet name and password). | | `mcp set-wallet [--target ] [--scope ] [--path ]` | Switch the wallet used by an already-installed Vulcan MCP server. | | `mcp diagnose [--target ] [--scope ] [--path ]` | Spawn the server with the exact command and env an agent client would use, run the JSON-RPC handshake, and assert `vulcan_*` tools appear. | | `log show [--limit ] [--session ]` | Show recent redacted action log records. | | `log summary [--limit ] [--session ]` | Summarize recent actions, positions, PnL, errors, and transactions. | | `log report [--limit ] [--session ]` | Build a position and session report from live trader state and local logs. | ```bash theme={null} vulcan agent install --target claude vulcan agent mcp install --target cursor --scope user --dangerous ``` ## `strategy` — strategy runners Long-running TWAP, grid, and TA strategy runners with ledger-backed pause / resume / finalize lifecycle. See the [Strategies](/cli/strategies) page for the full reference. | Subcommand | Description | | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `twap start ...` | Start a TWAP run. | | `twap resume [--from-step ]` | Resume a paused or incomplete TWAP run. | | `grid start ...` | Start a grid trading run. | | `grid resume [--from-step ]` | Resume a paused or incomplete grid run. | | `ta start ...` | Start a TA-driven strategy run from a config file or JSON. | | `ta resume ` | Resume a paused or incomplete TA run. | | `runs [--limit ]` | List persisted strategy runs. | | `status [--since-tick ] [--include-ledger]` | Show latest status for a run. | | `monitor [--include-ledger]` | Compact non-blocking monitor state. | | `wait-next-tick [--after-tick ] [--timeout-seconds ]` | Wait until a new tick or terminal status. | | `report ` | Show final or latest report. | | `reconcile-grid ` | Inspect live grid orders against the persisted ledger. | | `pause [--reason ]` | Request a running strategy to pause at the next safe point. | | `stop [--reason ]` | Request a strategy to stop permanently at the next safe point. | | `finalize [--reason ] [--cancel-orders] [--close-position] [--wait] [--timeout-seconds ]` | Stop a strategy and optionally clean up live orders or positions. | | `preflight` | Inspect live-readiness for the active wallet without launching anything: wallet identity, password availability, trader registration, collateral, and a remedy command for every blocker. | | `resume [--from-step ]` | Resume any paused or incomplete strategy run. | ## `ta` — technical analysis | Subcommand | Description | | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `compute --indicator [--timeframe ] [--period

] [--limit ] [--params ]` | Compute a single indicator over the latest candles. Supported indicators: `sma`, `ema`, `rsi`, `macd`, `bbands`, `atr`, `vwap`, `adx`, `stoch`. | | `signal --spec ` | Evaluate a trigger spec against the latest indicator value. Example spec: `{"indicator":"rsi","timeframe":"1h","op":"lt","threshold":30}`. | | `report [--timeframe ]` | Multi-indicator snapshot (RSI, MACD, BBands, ATR, ADX). | ```bash theme={null} vulcan ta compute SOL --indicator rsi --timeframe 1h vulcan ta compute SOL --indicator macd --params '{"fast":12,"slow":26,"signal":9}' vulcan ta report SOL --timeframe 4h -o json ``` ## Standalone commands | Command | Description | | ------------------------------------------- | ------------------------------------------------------------------- | | `status` | Check configuration, connectivity, wallet, and registration status. | | `setup` | Interactive setup wizard for wallet, config, and connectivity. | | `version` | Print version and build information. | | `update check [--force]` | Check whether a newer Vulcan release is available. | | `agent-context` | Print agent runtime context (CONTEXT.md) to stdout. | | `mcp [--allow-dangerous] [--groups ]` | Start the local MCP server over stdio. | ```bash theme={null} vulcan status -o json vulcan update check vulcan mcp --allow-dangerous --groups market,trade,position,margin ``` # CLI Source: https://docs.phoenix.trade/cli/index Agent and human-friendly CLI for trading Phoenix perpetual futures on Solana. Vulcan is the official command-line tool for trading [Phoenix](https://phoenix.trade) perpetual futures. It is designed for both human operators and AI agents, with structured JSON output, a local Model Context Protocol (MCP) server, and bundled skills for popular agent clients. The Vulcan repository is on GitHub: [Ellipsis-Labs/vulcan-cli](https://github.com/Ellipsis-Labs/vulcan-cli). Live commands execute irreversible financial transactions on Solana Mainnet. You are responsible for wallet security, agent permissions, and all trading outcomes. ## What you can do * Manage encrypted wallets, register and configure trader accounts, deposit and withdraw collateral. * Read live market data: prices, orderbooks, candles, funding rates, recent trades. * Compute technical indicators (RSI, MACD, Bollinger Bands, ATR, VWAP, ADX, Stoch, SMA, EMA) and evaluate triggers. * Place, cancel, and modify market and limit orders with optional take-profit and stop-loss. * Monitor and close positions across cross and isolated margin. * Run first-class strategy loops for [TWAP, grid, and TA-driven trading](/cli/strategies). * Trade in local paper mode against live prices with no real funds at risk. * Install [agent skills](/cli/installation#agent-skills) for Claude Code, Cursor, Codex, and other clients, then expose Vulcan tools through a local MCP server. ## Where to go next * [Installation](/cli/installation) — install the binary, configure your wallet, and verify connectivity. * [Command reference](/cli/commands) — every command group with its subcommands and flags. * [Strategies](/cli/strategies) — detailed reference for the TWAP, grid, and TA strategy runners. ## Output format Most commands support `-o table` (default) and `-o json`. Use JSON for scripting and agent integrations; structured command responses use a consistent success/error envelope: ```json theme={null} { "ok": true, "data": { }, "meta": { } } ``` ```json theme={null} { "ok": false, "error": { "category": "", "code": "", "message": "", "retryable": false } } ``` # Installation Source: https://docs.phoenix.trade/cli/installation Install the Vulcan CLI and set up your wallet, configuration, and agent integrations. ## Prerequisites * macOS or Linux. * `~/.local/bin` on your `PATH` (the default install location). * A Solana wallet, or you can let Vulcan generate one during setup. ## Install the latest release ```bash theme={null} curl -fsSL https://github.com/Ellipsis-Labs/vulcan-cli/releases/latest/download/install.sh | sh ``` The installer downloads the release archive, verifies it against `vulcan-checksums-sha256.txt`, and installs the binary to `~/.local/bin/vulcan` by default. Set `VULCAN_INSTALL_DIR` before running to install elsewhere. ## Install a specific version Replace the tag with any tagged release: ```bash theme={null} curl -fsSL https://github.com/Ellipsis-Labs/vulcan-cli/releases/download/v0.5.3/install.sh | sh ``` ## Build from source Requires Rust 1.84 or newer. From a clone of [Ellipsis-Labs/vulcan-cli](https://github.com/Ellipsis-Labs/vulcan-cli): ```bash theme={null} cargo install --path vulcan ``` ## Verify ```bash theme={null} vulcan version ``` ## First-run setup Run the interactive wizard to configure your wallet, RPC and API endpoints, registration, and optional first deposit: ```bash theme={null} vulcan setup ``` Then confirm everything is wired up: ```bash theme={null} vulcan status -o json ``` ## Configuration file Vulcan reads `~/.vulcan/config.toml`. A typical configuration looks like: ```toml theme={null} [network] rpc_url = "https://api.mainnet-beta.solana.com" api_url = "https://perp-api.phoenix.trade" # api_key = "your-api-key" [wallet] default = "my-wallet" [trading] default_slippage_bps = 50 confirm_trades = true ``` Any of `rpc_url`, `api_url`, or the default wallet can be overridden per-command with the matching global flag (`--rpc-url`, `--api-url`, `-w/--wallet`). ## Environment variables | Variable | Purpose | | ------------------------ | ------------------------------------------------------------------ | | `VULCAN_WALLET_NAME` | Stored wallet to use (defaults to the configured default wallet). | | `VULCAN_WALLET_PASSWORD` | Wallet unlock password. Required for non-interactive MCP sessions. | | `VULCAN_INSTALL_DIR` | Custom install directory used by the install script. | | `VULCAN_AGENT_TARGET` | Default agent client target for `vulcan agent install`. | | `VULCAN_AGENT_SCOPE` | Default install scope: `user` or `project`. | ## Agent skills Vulcan ships bundled skill files that teach AI agents how to use it safely. Install them for your client: ```bash theme={null} vulcan agent install --target claude vulcan agent install --target cursor vulcan agent install --target codex vulcan agent install --target agentskills ``` Supported targets: `claude`, `cursor`, `codex`, `agentskills`. Add `--scope project` for project-local installs where the target supports it. Inspect what is currently installed: ```bash theme={null} vulcan agent doctor --target claude vulcan agent health ``` End-to-end probe — spawn the server with the exact command and env an agent client would use and assert that `vulcan_*` tools come back: ```bash theme={null} vulcan agent mcp diagnose --target claude --scope user ``` If you migrate to a new Vulcan binary path and want to update `command` / `args` in an existing MCP entry without re-entering your wallet password: ```bash theme={null} vulcan agent mcp install --target claude --scope user --repair ``` ## MCP server Vulcan can run as a local Model Context Protocol (MCP) server over stdio so agents can call its tools directly. Private keys never leave the local Vulcan process. Read-only / paper-safe: ```bash theme={null} vulcan mcp ``` Live-capable: ```bash theme={null} export VULCAN_WALLET_NAME=my-wallet export VULCAN_WALLET_PASSWORD=your-password vulcan mcp --allow-dangerous ``` To have an agent client launch the MCP server automatically, install an MCP config: ```bash theme={null} vulcan agent mcp install --target cursor --scope user vulcan agent mcp install --target cursor --scope user --dangerous ``` The `--dangerous` form prompts for wallet name and password, then writes the values into the agent client's MCP config. To switch the wallet used by an already-installed MCP server: ```bash theme={null} vulcan agent mcp set-wallet --target claude --scope user ``` Continue to the [command reference](/cli/commands) or jump straight to [strategies](/cli/strategies). # Strategies Source: https://docs.phoenix.trade/cli/strategies First-class strategy runners: TWAP, grid, and TA-driven trading on Phoenix perpetuals. Vulcan's `strategy` command group hosts long-running execution loops that are too stateful for one-shot orders. Each run is ledger-backed, supports `pause` / `resume` / `finalize` lifecycle commands, and can be started in detached mode so the runner keeps ticking in the background. Tick logs and ledgers are persisted under `~/.vulcan/strategy-runs`. Three runners ship today: * **TWAP** — split a target size across timed slices. * **Grid** — maintain layered limit orders across a price band. * **TA** — rule-based runner over technical indicators for entry and exit. ## Execution modes Every strategy accepts `--mode`. Pick the one that matches your tolerance for live risk while testing: | Mode | Behavior | | -------------- | -------------------------------------------------------------------- | | `paper` | Simulated against live prices. No wallet or chain activity. Default. | | `dry-run` | Builds and logs each step but does not submit transactions. | | `confirm-each` | Submits live orders, prompting for confirmation before each step. | | `auto-execute` | Submits live orders without prompting. Use with guardrails. | ## Margin mode For TWAP and grid runs, `--margin-mode` selects how collateral is held: | Mode | Behavior | | ---------- | ---------------------------------------------------------------------------------------- | | `cross` | Uses the trader's main cross-margin account. Default. | | `isolated` | Opens an isolated subaccount. Pair with `--isolated-collateral` for the opening deposit. | ## Guardrails All three runners accept the same safety flags. Any breach pauses the run and records the reason in the ledger: | Flag | Description | | ------------------------------------ | ------------------------------------------------------------- | | `--max-total-notional-usdc ` | Cap on total live notional placed by the run. | | `--max-step-notional-usdc ` | Cap on a single tick's live notional. | | `--max-price-drift-bps ` | Pause if mark price drifts this far from the run-start price. | | `--max-exposure-ratio ` | Pause if position notional / equity exceeds this ratio. | | `--reconcile-attempts ` | Number of history reconciliation attempts per live step. | | `--reconcile-delay-ms ` | Milliseconds between reconciliation attempts. | ## TWAP TWAP slices a target size across N equally spaced intervals so each fill nudges the average price toward the time-weighted mean. ### `vulcan strategy twap start` | Flag | Description | | -------------------------------- | -------------------------------------------------------------------------------- | | `--symbol ` | Market symbol (e.g. `SOL`). | | `--side ` | Trade direction. | | `--notional-usdc ` | Total TWAP notional in USDC. Mutually exclusive with `--tokens`. | | `--tokens ` | Total TWAP size in base-asset tokens. Mutually exclusive with `--notional-usdc`. | | `--slices ` | Number of slices. | | `--interval-seconds ` | Seconds between slices. Default `60`. | | `--mode ` | Execution mode. Default `paper`. | | `--margin-mode ` | Margin mode. Default `cross`. | | `--isolated-collateral ` | USDC to transfer when opening the first isolated live order. | | `--run-label

` | Lower grid boundary price. Required unless `--center-on-mark` is set. | | `--upper-price

` | Upper grid boundary price. Required unless `--center-on-mark` is set. | | `--center-on-mark` | Center the grid on the current mark price at launch. Requires `--width-pct`. | | `--width-pct ` | Half-width of the grid as a percentage of mark (e.g. `1.0` = ±1%). Requires `--center-on-mark`. | | `--levels-per-side ` | Number of buy levels below mark and sell levels above mark. | | `--tokens-per-level ` | Order size per level in base-asset tokens. Mutually exclusive with `--size-lots-per-level`. | | `--size-lots-per-level ` | Order size per level in base lots. Mutually exclusive with `--tokens-per-level`. | | `--bid-level PRICE:SIZE_LOTS[:TP][:SL]` | Fully custom bid level. Repeatable, replaces generated bids. | | `--ask-level PRICE:SIZE_LOTS[:TP][:SL]` | Fully custom ask level. Repeatable, replaces generated asks. | | `--take-profit-spacing

` | Distance from entry to take-profit for generated levels. | | `--stop-loss-spacing

` | Distance from entry to stop-loss for generated levels. | | `--interval-seconds ` | Seconds between maintenance ticks. Default `60`. | | `--ticks ` | Maximum ticks to run, including the initial placement tick. Default `60`. | | `--run-until-stopped` | Keep running maintenance ticks until paused or stopped. | | `--stale-after-seconds ` | Seconds without a tick before status reports this run as stale. | | `--mode ` | Execution mode. Default `paper`. | | `--margin-mode ` | Margin mode. Default `cross`. | | `--isolated-collateral ` | USDC to transfer when opening the first isolated live order. | | `--run-label

Phoenix
## What is Phoenix? Phoenix is a non-custodial, decentralized exchange for perpetual futures built on the Solana blockchain. Maintain full control of your assets at all times Built on Solana for fast, transparent transactions Trade perpetual contracts with leverage Contracts have no date expiration *** ## Accessing Phoenix Phoenix is available at [phoenix.trade](http://phoenix.trade). Phoenix is not available in the U.S. or sanctioned jurisdictions. ## Connect Wallet and Fund Account Before you begin, make sure you can buy and withdraw USDC from a cryptocurrency exchange. Phoenix currently accepts USDC as collateral. **Recommended for first-time users** Select **Start Trading**, then log in with Google or enter your email. Phoenix creates a wallet for you through Privy, so you do not need to install a wallet extension. **Recommended browser wallets** Install [Solflare](https://www.solflare.com/download/), [Phantom](https://phantom.com/download), or [Backpack](https://backpack.app/download), then create a wallet. If this is your first browser wallet, follow [Solflare's step-by-step setup guide](https://www.solflare.com/guides/how-to-set-up-your-first-crypto-wallet-with-solflare-step-by-step/). Then select **Start Trading**, select **Connect new wallet**, and choose your wallet. Buy USDC from a cryptocurrency exchange, withdraw it to the wallet you use with Phoenix, then deposit it from that wallet into Phoenix: 1. Buy USDC on an exchange. 2. Copy your wallet address from the Phoenix account menu. On the exchange, withdraw your USDC to that address and select **Solana** as the withdrawal network. 3. After the USDC arrives in your wallet, return to Phoenix and select **Deposit**. 4. Enter the amount, select **Submit Deposit**, and follow the on-screen prompts to confirm the deposit. After funding your account: 1. Select the market you want to trade. 2. Choose **Market** to trade at available prices, or **Limit** to set a price. A limit order may remain open if it cannot fill at your price or better. 3. Choose **Long/Buy** or **Short/Sell**, then enter the size. For a limit order, also enter the **Limit Price**. 4. Review the order details, select **Place Order**, and confirm the order. Learn more: [Order Types](phoenix/matching-engine/order-types), [Fees](phoenix/matching-engine/fees), and [Risk Warning](phoenix/margin-and-risk/risk-warning). # Accounts Source: https://docs.phoenix.trade/phoenix/collateral-and-accounts/accounts How Phoenix accounts work and how cross and isolated risk differ Phoenix separates a wallet authority from its trader accounts. The wallet signs transactions. Trader accounts hold collateral, positions, and resting orders. Each trader account is a program-derived address (PDA) derived from: * wallet authority * `portfolio_index` * `subaccount_index` The pair `(portfolio_index, subaccount_index)` defines which account is being addressed for that authority. ## Authority and position authority Each trader account has an `authority` and a `position_authority`. The `authority` is the wallet authority used to derive and own the trader account. It has full account control: it can deposit and withdraw collateral, place and cancel orders, manage stop losses and conditional orders, create and sync subaccounts, and update the account's `position_authority`. The `position_authority` is a trading signer stored on the trader account. New trader accounts start with `position_authority` set to the same wallet as `authority`, but the authority can delegate it to another wallet, such as an embedded app wallet. A position authority can sign trading actions like placing orders, cancelling orders, and managing stop losses or other conditional orders. A `position_authority` is not a collateral owner. It cannot withdraw collateral from Phoenix and cannot call authority-only account management instructions such as `WithdrawFunds`, `DepositFunds`, or `DelegateTrader`. Some collateral transfers between trader accounts under the same wallet authority may allow a position authority signer, but a position authority cannot move collateral out to the wallet. To set or rotate this signer from an integration, see [Set position authority](/sdk/accounts#set-position-authority) in the SDK docs. ## Portfolio index `portfolio_index` partitions a wallet's Phoenix state into independent portfolios. Each `portfolio_index` has its own collateral, positions, orders, and risk. Positions and collateral in one portfolio do not margin positions in another portfolio. This lets a single wallet authority maintain separate trading books without sharing collateral or liquidation risk between them. ## Subaccount index Within a portfolio, `subaccount_index` selects either the cross account or an isolated account. ### Cross account `subaccount_index = 0` is the cross account for the portfolio. The cross account uses one shared collateral pool across its active positions. PnL, funding, margin requirements, and order margin all affect the same account health. Capacity limits: * up to `128` active positions * up to `64` resting bid limit orders per market * up to `64` resting ask limit orders per market ### Isolated accounts `subaccount_index > 0` is an isolated account under the same portfolio. An isolated account is used for a single isolated position. Collateral is moved from the portfolio's cross account into the isolated account when the isolated position is opened or funded. The isolated account has its own collateral and liquidation boundary. Losses in the isolated account do not automatically draw from the cross account after collateral has been allocated. When the isolated position is closed, a crank can sweep remaining collateral back from the isolated account to the portfolio's cross account. ## Account model For a given wallet authority: * `(portfolio_index = 0, subaccount_index = 0)` is the first portfolio's cross account * `(portfolio_index = 0, subaccount_index > 0)` is an isolated account under the first portfolio * `(portfolio_index = 1, subaccount_index = 0)` is a separate cross account with independent collateral and positions * `(portfolio_index = 1, subaccount_index > 0)` is an isolated account under that separate portfolio The important boundary is the `portfolio_index`. Subaccounts inside one portfolio relate to that portfolio's cross account. Different portfolios do not share margin. # Collateral Source: https://docs.phoenix.trade/phoenix/collateral-and-accounts/collateral How Phoenix uses USDC and SOL collateral Phoenix perps settle PnL, funding, and trading fees in USDC. Phoenix supports both USDC and native SOL as collateral. SOL contributes a risk-adjusted USDC value to your account health. You can deposit, withdraw, and transfer both assets through the Phoenix app. ## Collateral assets Phoenix supports USDC and native SOL as collateral. Other collateral assets may be added in the future. Solana USDC is deposited through the Ember contract first before it reaches the Phoenix exchange. Ember wraps Solana USDC `1:1` into Phoenix's exchange-side collateral representation. Withdrawals perform the reverse conversion. ## Ember Ember is the proxy contract between standard Solana USDC and Phoenix collateral. Deposits: 1. USDC moves from your wallet token account into Ember. 2. Ember wraps the same atomic amount `1:1`. 3. Phoenix credits the collateral to your trader account. Withdrawals: 1. Phoenix withdraws collateral from your trader account. 2. Ember unwraps the same atomic amount `1:1`. 3. USDC returns to your wallet. Ember does not set exchange rates, apply haircuts, or introduce another collateral asset. The amount transferred into Ember is the amount credited through the wrapper. The amount burned on withdrawal is the amount released back as Solana USDC. ## Account collateral and effective collateral `deposited_collateral` is the USDC collateral balance credited to a Phoenix trader account. `effective_collateral` is the collateral value Phoenix uses for risk checks. It includes deposited USDC, the risk-adjusted value of SOL collateral, unsettled funding, and unrealized PnL: $$ C_{\text{effective}} = C_{\text{deposited}} + V_{\text{SOL, margin}} + U + F_{\text{unsettled}} $$ Effective collateral is not a separate balance or token. It is the margin value used for health checks, liquidations, and withdrawal eligibility. ## SOL margin value Phoenix counts up to `80%` of your SOL's value at the SOL index price toward margin. If Phoenix cannot obtain a valid SOL index price, actions that depend on SOL collateral valuation may be unavailable until valid pricing resumes. Let: * $B$ be your SOL collateral balance * $P_{\text{SOL}}$ be the SOL index price in USDC * $w_{\text{SOL}}$ be the current SOL collateral weight, up to `0.80` The undiscounted value is: $$ N_{\text{SOL}} = B \times P_{\text{SOL}} $$ The value that contributes to margin is: $$ V_{\text{SOL, margin}} = N_{\text{SOL}} \times w_{\text{SOL}} $$ At the maximum `80%` collateral weight, the remaining `20%` is a risk-management buffer for SOL price changes and execution slippage if the collateral must be sold for USDC. It is not a fee: your SOL balance does not change, and portfolio value still uses the full index-price value. Phoenix may limit or temporarily disable SOL deposits as risk conditions change. The app shows whether SOL deposits are currently available. ## Withdrawals Phoenix rate-limits protocol-wide collateral outflows with a global withdraw queue. The queue applies to exchange-wide withdrawals, not just activity on one account. * A withdrawal clears immediately if the queue is empty and the request fits within the current withdrawal budget. * Otherwise, the withdrawal is queued. * Queued withdrawals are processed in FIFO order as budget replenishes over time. * Queued withdrawals are not partially filled. * If the account no longer has enough withdrawable collateral when the request reaches the front of the queue, the request can be dropped instead of processed. The Phoenix app charges a flat `0.50 USDC` fee for each USDC withdrawal, including withdrawals that enter the queue. This fee is specific to the app and is not charged protocol-wide. Negative unrealized PnL on an open position does not independently block withdrawals. Phoenix already includes that loss in its margin checks. Phoenix blocks both USDC and SOL withdrawals from a cross account when a realized loss leaves its USDC balance negative. To unblock withdrawals, deposit enough USDC to restore a nonnegative balance or sell SOL collateral for USDC. ### SOL withdrawals SOL withdrawals have no withdrawal fee. They must leave your account at or above its initial margin requirement. SOL withdrawals are processed immediately or fail; they are never queued. A withdrawal may fail while the withdrawal queue is active or when protocol-wide withdrawal capacity is unavailable. ## Using SOL collateral SOL contributes to margin for orders and open positions. However, it does not increase your USDC withdrawal limit. For a USDC withdrawal, Phoenix evaluates your account without the margin value of SOL: $$ C_{\text{USDC withdrawal}} = C_{\text{deposited}} + U_{\text{withdrawal-discounted}} + F_{\text{unsettled}} $$ If your projected USDC balance becomes sufficiently negative relative to your SOL margin value, Phoenix may automatically convert some SOL to USDC. See [Automatic SOL conversion](/phoenix/margin-and-risk/liquidations#automatic-sol-conversion) for the trigger and sizing math. ## Reference ### Contract and mint addresses * Ember program id: `EMBERpYNE6ehWmXymZZS2skiFmCa9V5dp14e1iduM5qy` * Wallet USDC mint: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` * Phoenix USDC mint: `PhUsd11YkbjSaWjFncfAAmatntsjx3MgDR9B6g1ks3A` ### Current withdraw parameters Snapshot verified from live mainnet exchange accounts on `2026-04-20`: * `max_budget`: `2,000,000` Phoenix USDC * `replenish_amount_per_slot`: `450` Phoenix USDC per slot * `withdrawal_fee`: `0` Phoenix USDC * SOL withdrawal fee: `0` * `enqueueing_fee`: `0` * `deposit_cooldown_period_in_slots`: `0` # Cross-chain Deposits Source: https://docs.phoenix.trade/phoenix/collateral-and-accounts/cross-chain-deposits How to deposit source tokens to Phoenix from other chains Phoenix collateral lives on Solana as USDC. If your source token is on another chain, Phoenix routes the deposit through [Relay](https://relay.link), which swaps and bridges the funds to Solana USDC before crediting your trader account. ## Supported source chains Cross-chain deposits are currently supported from: * Ethereum * Base * Arbitrum * HyperEVM ## How it works 1. You select a supported source chain and source token in the deposit UI. 2. The UI displays a sample quote from Relay, including the amount of Solana USDC you will receive after fees. 3. After you confirm, Relay executes the swap and bridges the funds to Solana. 4. The resulting Solana USDC is deposited through Ember and credited to your Phoenix trader account. ## Quotes The sample quote shown in the UI is provided by Relay and varies based on the cost of gas on the source chain at the time of the request. The final amount credited may differ slightly from the sample quote if gas prices move between when the quote is displayed and when the transaction is executed. ## Troubleshooting If a cross-chain deposit does not arrive or you run into any issues, file a ticket in the [Phoenix Discord](https://discord.gg/phoenixtrade) and include your source-chain transaction signature. The team can use the signature to trace the deposit with Relay and resolve the issue. # FAQ Source: https://docs.phoenix.trade/phoenix/faq/faq Answers to the most common execution, margin, and account questions ## Why can effective collateral differ from my deposited collateral? Effective collateral is the value Phoenix uses for risk checks. It is not a separate balance or token. At a high level: $$ C_{\text{effective}} = C_{\text{deposited}} + V_{\text{SOL, margin}} + U^{+}_{\text{discounted}} + U^{-} + F_{\text{unsettled}} $$ Deposited collateral is the USDC collateral balance on the trader account. SOL separately contributes up to `80%` of its value at the SOL index price. If Phoenix cannot obtain a valid SOL index price, actions that depend on SOL collateral valuation may be unavailable until valid pricing resumes. Positive unrealized PnL may be discounted before it counts toward margin, negative unrealized PnL counts in full, and unsettled funding can move effective collateral up or down before it is reflected as settled collateral. See [Collateral](/phoenix/collateral-and-accounts/collateral) for the account-level collateral model and [Margin Math](/phoenix/margin-and-risk/margin-math) for the risk calculation. ## Do subaccounts share risk? It depends on the account type. Within a portfolio, `subaccount_index = 0` is the cross account. Positions, collateral, funding, PnL, and resting-order margin all feed into one shared account health calculation. `subaccount_index > 0` is used for isolated accounts. Collateral is moved from the portfolio's cross account into the isolated account, and that isolated account has its own liquidation boundary. Losses in the isolated account do not automatically pull more collateral from the cross account after collateral has been allocated. Unused or remaining collateral in the isolated account may be swept back to the cross account. Different `portfolio_index` values are separate portfolios. They do not share collateral, positions, or liquidation risk. See [Accounts](/phoenix/collateral-and-accounts/accounts) for the full account model. ## Are builder fees the same as trading fees? No. Flight builder fees are separate from Phoenix maker/taker fees and are additive when an order is routed through a builder. See [Fees](/phoenix/matching-engine/fees) and [Builder Codes](/builder-codes). ## Why does positive PnL not fully count toward margin? Positive unrealized PnL is not the same as deposited collateral. It depends on the current mark price, and that mark can move. Phoenix can discount positive unrealized PnL before counting it toward effective collateral. Negative unrealized PnL counts in full. The discount reduces the risk that unstable or manipulated mark prices create too much usable collateral. This matters more for volatile or lower-liquidity assets, where mark prices can be easier to move or temporarily distorted, so the market-specific discount can differ by asset. Market-specific `uPnL risk factor` values are shown in [Market Parameters](/phoenix/market-parameters). See [Margin Math](/phoenix/margin-and-risk/margin-math) for the effective collateral calculation. ## Can I withdraw while an open position has negative unrealized PnL? Negative unrealized PnL does not independently block a withdrawal. Phoenix already includes the open loss in its margin checks, so the withdrawal can proceed if your account still meets its margin requirements. If you close the position and the realized loss leaves your cross account with a negative USDC balance, Phoenix blocks both USDC and SOL withdrawals. To unblock them, deposit enough USDC to restore a nonnegative balance or sell SOL collateral for USDC. See [Collateral](/phoenix/collateral-and-accounts/collateral#deposits-and-withdrawals) for withdrawal fees and other restrictions. ## Why did Phoenix convert my SOL collateral to USDC? Phoenix can automatically convert SOL collateral when your projected USDC balance is negative and the debt is large relative to the discounted margin value of your SOL. The projected USDC balance includes unsettled funding but excludes unrealized PnL. For cross accounts, Phoenix can convert SOL when the projected debt is strictly greater than `50%` of the discounted SOL value. When you close an isolated position, any negative unrealized PnL becomes a loss in the account's USDC balance. Phoenix sells deposited SOL collateral to cover that loss even when the ratio is `50%` or lower. Automatic SOL conversion changes your collateral; it does not close an open position. Phoenix targets enough SOL to cover the projected USDC debt plus a small cushion, subject to current risk constraints and available swap liquidity. See [Risk Tiers And Liquidation](/phoenix/margin-and-risk/liquidations#automatic-sol-conversion) for the trigger and sizing math. ## Why did my liquidation price move when I did not touch that market? Liquidation price is an estimate, not a fixed promise. Phoenix estimates liquidation price from your collateral, position size, entry price, mark price, leverage tier, funding, and the rest of the account. In cross margin, other positions can change effective collateral and maintenance requirements, so the estimated liquidation price for one market can move even if you did not trade that market. Common reasons it moves: * Mark price changes * Funding accrues or settles * Another cross-margin position gains or loses value * Resting risk-increasing orders change margin requirements * The market's leverage tier or risk parameters change See [Margin Math](/phoenix/margin-and-risk/margin-math), [Mark Price](/phoenix/margin-and-risk/mark-price), and [Funding](/phoenix/margin-and-risk/funding-rate). ## Why was my resting order cancelled before I was liquidated? Phoenix can cancel risk-increasing resting limit orders before liquidating filled positions. This can happen when effective collateral falls below cancel margin. A resting order is risk-increasing if it could add exposure or increase required margin when filled. Cancelling it can improve account health without touching open positions. See [Risk Tiers And Liquidation](/phoenix/margin-and-risk/liquidations), [Account Health](/phoenix/margin-and-risk/account-health), and [Leverage Tiers](/phoenix/margin-and-risk/leverage-tiers). ## What is the difference between liquidation and ADL? * liquidation tries to close the risky account through normal market or backstop paths * ADL reduces profitable counterparties when the exchange needs a stronger last-resort protection See [Risk Tiers And Liquidation](/phoenix/margin-and-risk/liquidations). # Support Source: https://docs.phoenix.trade/phoenix/faq/support Get help and stay updated ## General Support For trading FAQs, support, or to open a help ticket, join the Phoenix Discord: [discord.gg/phoenixtrade](https://discord.gg/phoenixtrade) Follow Phoenix on X for updates and announcements: [x.com/phoenixtrade](https://x.com/phoenixtrade) ## Connect Discord The best way to get support and share feedback is through Discord. After signing up for Phoenix, connect your Discord account through the menu in the top right to unlock the feedback channels. Image(12) # Glossary Source: https://docs.phoenix.trade/phoenix/glossary Key Phoenix trading, execution, and risk terms **Account health:** The overall risk condition of a trader account, computed from effective collateral versus margin thresholds. **ADL:** Auto-deleveraging. A last-resort mechanism that reduces profitable counterparties when liquidation and backstop paths are not enough. **Automatic SOL conversion:** The sale of SOL collateral to cover a projected negative USDC balance. This changes collateral and does not close a perpetual position. **Backstop liquidation:** A more severe liquidation path used after the ordinary maintenance/liquidation threshold has already been breached. **Builder fee:** The additional fee charged when an order is routed through Flight. **Cancel margin:** The threshold below which Phoenix can force-cancel risk-increasing resting orders. **Discounted unrealized PnL:** Positive unrealized PnL after applying the market's uPnL risk factor. Negative unrealized PnL is counted in full. **Effective collateral:** Deposited collateral plus discounted SOL collateral value, discounted unrealized PnL, and unsettled funding: $$ C_{\text{effective}} = C_{\text{deposited}} + V_{\text{SOL, margin}} + U_{\text{discounted}} + F_{\text{unsettled}} $$ **Entry price:** The effective average basis of the active position. Phoenix derives it from virtual quote position divided by active base position. **FIFO order book:** The normal resting order book where better price fills first and equal-price orders fill in time order. **Funding:** Periodic cash flow between longs and shorts based on mark-index basis. **High-risk margin:** The deepest standard threshold before ADL-style handling becomes relevant. **Index price:** The external spot reference used as the anchor for funding and mark-price construction. **Initial margin:** The collateral requirement to open or safely maintain current exposure, based on leverage tiers and order margin. **Isolated margin:** A margin mode where a child account has its own dedicated collateral pool. **Liquidation:** Forced risk reduction once effective collateral falls below maintenance requirements. **Maintenance margin:** The minimum effective collateral needed before normal liquidation begins. **Mark price:** The reference price Phoenix uses for PnL, liquidations, funding, and trigger orders. **Open interest:** The total outstanding long and short exposure in a market. **Pending funding / unsettled funding:** Funding that has accrued but has not yet been formally settled into collateral. **PnL:** Profit and loss. **Risk score:** A numerical health metric used across Phoenix state tooling. Lower is healthier; around `1000` is near liquidation. **Self-trade prevention:** Matching-engine behavior that prevents a trader from filling against their own resting liquidity. **SOL collateral haircut:** At the maximum collateral weight, Phoenix applies a `20%` reduction to SOL's index value and the remaining `80%` contributes to effective collateral. If Phoenix cannot obtain a valid SOL index price, actions that depend on SOL collateral valuation may be unavailable until valid pricing resumes. **Spline liquidity:** Phoenix's region-based liquidity model around a spline mid price. **Subaccount:** A child account under the same wallet authority. In Phoenix, subaccounts above `0` are used for isolated risk. **Unrealized PnL:** Profit or loss on an open position at the current mark price. # Account Health Source: https://docs.phoenix.trade/phoenix/margin-and-risk/account-health How Phoenix turns collateral and margin into health states and risk tiers Phoenix expresses risk both as named tiers and as a numerical risk score. ## Risk tiers | Tier | Meaning | | ---------------------- | ----------------------------------------------------- | | `Safe` | Effective collateral is above initial margin | | `AtRisk` | Below initial margin but not yet cancellable | | `Cancellable` | Risk-increasing resting orders can be force-cancelled | | `Liquidatable` | Market-order liquidation can begin | | `BackstopLiquidatable` | The account is beyond normal liquidation comfort | | `HighRisk` | Deeply stressed and potentially ADL-eligible | ## Risk score A common Phoenix state score is: * if maintenance margin is $0$, the score is $0$ * if effective collateral is positive, the score is approximately $\left(\frac{\text{maintenance margin}}{\text{effective collateral}}\right) \times 1000$ * if effective collateral is zero or negative, the score rises above $1000$ with an underwater penalty Interpretation: * lower is healthier * around $1000$ means the account is at or through the liquidation boundary * above $1000$ means the account is underwater ## What improves health * adding collateral * reducing positions * cancelling risk-increasing resting orders * receiving positive funding * positive mark-price movement on your exposure ## What worsens health * adverse price moves * paying funding * adding more size * placing more risk-increasing orders * moving collateral out of the account ## Why the same position can have a different health state tomorrow Because health is portfolio-aware and dynamic: * mark price changes * funding changes * other positions in cross margin change * positive-uPnL credit can be discounted # Entry Price And PnL Source: https://docs.phoenix.trade/phoenix/margin-and-risk/entry-price-pnl How Phoenix tracks entry price, unrealized PnL, and realized PnL Phoenix derives entry price from the open position's base size and virtual quote position. For an open position: $$ \text{entry price} = \frac{\left|\text{virtual quote lot position}\right|} {\left|\text{base lot position}\right|} $$ If `base_lot_position` is zero, the position has no active entry price. ## When entry price changes Entry price changes when a fill increases exposure. * if you are flat or long and buy more, Phoenix recalculates the long entry price * if you are flat or short and sell more, Phoenix recalculates the short entry price * maker, taker, and spline fills follow the same accounting rules Reducing a position does not change the remaining entry price. It realizes PnL on the closed portion and preserves the average basis for the open remainder. A full close removes the entry price. A flip through zero creates a new entry price for the residual position. ## PnL Unrealized PnL is based on mark price versus entry price: $$ \text{unrealized PnL} = \text{position size} \times \left(\text{mark price} - \text{entry price}\right) $$ * long positions gain when mark price rises * short positions gain when mark price falls Realized PnL is created when a fill reduces, closes, or flips an existing position. ## What does not change entry price The following do not change entry price by themselves: * placing a resting limit order * funding settlement * trading fees * builder fees Funding and fees affect collateral separately. They are not included in `virtual_quote_lot_position`. If an opposite-side limit order rests, entry price is unchanged. If it executes, the filled portion follows the normal fill rules above. ## See also * [Mark Price](/phoenix/margin-and-risk/mark-price) * [Funding](/phoenix/margin-and-risk/funding-rate) * [Fees](/phoenix/matching-engine/fees) # Funding Source: https://docs.phoenix.trade/phoenix/margin-and-risk/funding-rate Funding rates keep perpetual prices aligned with spot prices through periodic payments between traders. ## How It Works $$ \text{funding} = \left(\text{mark price} - \text{index price}\right) \times \text{rate} $$ * **When $\text{mark price} > \text{index price}$:** Longs pay shorts (incentivizes selling, reduces perp price) * **When $\text{mark price} < \text{index price}$:** Shorts pay longs (incentivizes buying, raises perp price) ## Settlement * Funding accumulates continuously based on hourly snapshots and settles every 24 hours * Pending funding (but unsettled) can affect account health/liquidation risk in real time * The maximum funding rate is clamped per market. The percentage is dependent on mark price and can be found in the [Market Parameters](/phoenix/market-parameters). When a trader interacts with the protocol, their pending funding is calculated as: $$ \text{funding payment} = \left(\text{current accumulator} - \text{snapshot accumulator}\right) \times \text{position size} $$ # Leverage Tiers Source: https://docs.phoenix.trade/phoenix/margin-and-risk/leverage-tiers How position size affects max leverage and margin Phoenix uses market-specific leverage tiers. As position size increases, the market may require more collateral for the same notional exposure. Smaller positions can receive higher max leverage. Larger positions may receive lower max leverage. ## How tiers affect margin Initial margin is based on position size, mark price, and the max leverage available at that size: $$ \text{initial margin} = \frac{\text{position notional}}{\text{maximum leverage}} $$ If a fill increases your total position size into a lower-leverage range, Phoenix recalculates margin for the full resulting position, not only the newest fill. Reducing position size can lower margin because: * the notional position is smaller * the remaining size may qualify for higher max leverage ## Resting orders Risk-increasing resting limit orders can also reserve margin before they fill. Phoenix evaluates whether resting bids or asks could increase exposure. If they can, the account may need additional collateral even though the orders have not executed yet. Reduce-only orders do not add exposure. ## Live tier values Leverage tiers are market-specific and can change. Use [Market Parameters](/phoenix/market-parameters) for current tier values, including: * max leverage * max size * limit order risk factor # Risk Tiers And Liquidation Source: https://docs.phoenix.trade/phoenix/margin-and-risk/liquidations Risk classification, automatic SOL conversion, and liquidation types Phoenix classifies trader accounts by comparing effective collateral against margin thresholds. As collateral falls, the account moves into progressively more severe risk tiers. Each tier gives the protocol stronger tools to reduce risk. ## Risk tiers | Tier | Status | Condition | Action | | ---- | -------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------- | | 0 | Low Risk | $\text{effective collateral} \geq \text{initial margin}$ | No immediate action | | 1 | At Risk | $\text{cancel margin} < \text{effective collateral} < \text{initial margin}$ | Monitored | | 2 | Cancellable | $\text{maintenance margin} < \text{effective collateral} \leq \text{cancel margin}$ | Risk-increasing limit orders can be cancelled | | 3 | Liquidatable | $\text{effective collateral} < \text{maintenance margin}$ | Positions are eligible for market liquidation | | 4 | BackstopLiquidatable | $\text{effective collateral} < \text{backstop threshold}$ | Backstop liquidation can transfer distressed risk | | 5 | High Risk | $\text{effective collateral} < \text{high-risk threshold}$ | ADL eligible | ## Automatic SOL conversion Automatic SOL conversion is separate from perpetual-position liquidation. It converts SOL collateral to USDC when your projected USDC debt becomes large relative to the margin value of your SOL. Phoenix calculates your projected USDC balance without unrealized PnL: $$ Q_{\text{projected}} = C_{\text{USDC}} + F_{\text{unsettled}} $$ An unrealized loss on an open position does not trigger automatic SOL conversion. The loss affects this calculation after you close the position and it becomes part of your USDC balance. If $Q_{\text{projected}}$ is negative, the projected debt is: $$ D = \left|Q_{\text{projected}}\right| $$ Phoenix divides that debt by the [discounted margin value of your SOL](/phoenix/collateral-and-accounts/collateral#sol-margin-value): $$ \text{debt ratio} = \frac{D}{V_{\text{SOL, margin}}} $$ By default, Phoenix can convert SOL in a cross account when this ratio is strictly greater than `50%`. A ratio of exactly `50%` does not qualify. An isolated account can qualify even below `50%` after its position closes. Any negative unrealized PnL becomes a loss in the account's USDC balance, and Phoenix sells deposited SOL collateral to cover it. Cross accounts always use the `50%` threshold, even when they have no open positions. ### Conversion amount Phoenix targets the projected debt plus a cushion of no more than `0.1%`. For example, if the projected debt is `1,000 USDC`, the target is no more than `1,001 USDC`. Phoenix values the SOL at its index price, applies the conversion discount, and targets the smallest amount expected to provide the required USDC. Current risk constraints and available swap liquidity may limit the amount or delay execution. ## Liquidation types ### Market liquidation For liquidatable accounts, Phoenix can reduce positions through market execution. * executes against available liquidity * can be partial * must improve trader health or fully close the position * respects market liquidation size limits ### Backstop liquidation If market liquidation is not enough, Phoenix can transfer a distressed position to a backstop account. * the backstop account absorbs the position * the backstop account then unwinds the risk through normal market execution * this path is used when orderbook liquidity is insufficient or the account is too distressed for ordinary liquidation ### ADL ADL is the last-resort path. * the losing position is matched against a profitable trader on the opposite side * the highest-priority profitable trader is selected first * the match closes or reduces both sides * profitable traders affected by ADL may realize less profit than expected # Margin Math Source: https://docs.phoenix.trade/phoenix/margin-and-risk/margin-math How Phoenix calculates effective collateral, position margin, and limit-order margin Phoenix evaluates each trader account as a single margin account. Positions, funding, unrealized PnL, and risk-increasing resting orders all feed into the same account health calculation. When you deposit SOL, its risk-adjusted value also contributes to account health. Margin is the mechanism that lets traders take positions larger than their posted collateral while keeping the exchange solvent. A perp venue always has a long and a short side for the same open interest. When one side has unrealized profit, the other side has the matching liability. The risk system is designed to keep those liabilities collateralized and to make sure risky positions can be reduced before they create bad debt. At a high level: $$ \text{account health} = \text{effective collateral} - \text{margin requirement} $$ If effective collateral falls through the risk thresholds, Phoenix can cancel risk-increasing orders, liquidate through the order book, escalate to backstop liquidation, and finally use ADL. ## Risk invariants Phoenix margin is built around three practical invariants: * long and short open interest must balance * unrealized profits must be matched by liabilities elsewhere in the system * those liabilities must remain collateralized If a losing account can no longer cover the unrealized profit owed to the other side, the system has bad debt. Margin requirements, mark prices, funding, liquidation thresholds, and open interest controls all exist to reduce the probability of that outcome. ## Effective collateral Effective collateral is the collateral value Phoenix uses for risk checks. It is similar to account equity, but Phoenix can discount positive unrealized PnL before treating it as usable collateral. Phoenix also applies a haircut to SOL collateral. $$ \begin{aligned} \text{effective collateral} ={}& \text{deposited collateral} \\ &+ \text{discounted SOL collateral value} \\ &+ \text{discounted positive unrealized PnL} \\ &+ \text{negative unrealized PnL} \\ &+ \text{unsettled funding} \end{aligned} $$ Important details: * deposited collateral is the USDC collateral balance on the trader account * SOL collateral contributes up to `80%` of its value at the SOL index price * positive unrealized PnL can be discounted by the market's `uPnL risk factor` * negative unrealized PnL counts in full * unsettled funding is included because it changes account risk before final settlement Phoenix counts up to `80%` of your spot SOL's index value toward margin. At the maximum weight, the `20%` haircut is a risk-management buffer for SOL price changes and execution slippage if the collateral must be sold for USDC. Phoenix discounts positive uPnL to reduce the risk that manipulated or unstable mark prices create too much usable collateral. This matters more for volatile or lower-liquidity assets, where mark prices can be easier to move temporarily. See [Market Parameters](/phoenix/market-parameters) for market-specific `uPnL risk factor` values. See [Collateral](/phoenix/collateral-and-accounts/collateral#sol-margin-value) for the SOL valuation formula. ## Mark price and PnL Position value is based on mark price, not the last trade price. $$ \text{unrealized PnL} = \text{position size} \times \left(\text{mark price} - \text{entry price}\right) $$ Using mark price for PnL, margin, and liquidation checks helps prevent a single trade or thin order book from pushing accounts into unsafe states. See [Mark Price](/phoenix/margin-and-risk/mark-price) for how Phoenix builds the mark. ## Position margin For each market, Phoenix calculates margin from the absolute position size and the market's leverage tier. $$ \text{position notional} = \left|\text{position size}\right| \times \text{mark price} $$ $$ \text{position margin} = \frac{\text{position notional}}{\text{maximum leverage for size}} $$ The leverage tier is selected from the resulting position size. A fill that moves the total position into a different tier can change margin for the whole resulting position, not only the newest fill. ## Limit-order margin Risk-increasing resting limit orders can require margin before they fill. Phoenix evaluates resting bids and resting asks separately for each market: $$ M_{\text{market, initial}} = M_{\text{position}} + \max\left(\Delta M_{\text{bid}},\ \Delta M_{\text{ask}}\right) $$ This matters because a trader cannot fill every bid and every ask in the same market into the same final directional exposure. Phoenix therefore reserves margin for the worse side, not the sum of both sides. The margin increase for each side is based on the hypothetical position if those non-reduce-only orders fill: $$ \Delta M_{\text{side}} = \left(M_{\text{post-fill}} - M_{\text{position}}\right) \times f_{\text{limit order}} $$ Notes: * reduce-only orders do not add margin * orders that only reduce or close the current position do not add margin * limit-order margin uses the market's mark price and size, not the order's limit price * `limit_order_risk_factor` is market- and tier-specific * limit-order margin is part of initial margin and can move an account toward the cancellation threshold before the order fills See [Leverage Tiers](/phoenix/margin-and-risk/leverage-tiers) and [Market Parameters](/phoenix/market-parameters). ## Example portfolio Assume a cross account has `12,000 USDC`, no SOL collateral, and the following open positions: | Market | Position | Entry | Mark | Unrealized PnL | | ------ | --------------- | -------- | -------- | -------------- | | BTC | Long `0.10 BTC` | `60,000` | `62,000` | `+200 USDC` | | SOL | Short `100 SOL` | `150` | `140` | `+1,000 USDC` | Using the fallback market parameters for BTC and SOL, positive uPnL receives `100%` collateral credit for trading risk checks. Assume unsettled funding is `-50 USDC`. $$ \begin{aligned} \text{discounted positive unrealized PnL} &= \left(200 + 1{,}000\right) \times 1.00 \\ &= 1{,}200.000000 \end{aligned} $$ $$ \begin{aligned} \text{effective collateral} &= 12{,}000 + 1{,}200 - 50 \\ &= 13{,}150.000000\ \text{USDC} \end{aligned} $$ Now calculate position margin using the first leverage tier from `phoenix/market-parameters-fallback.json`. Values below include the quote-lot rounding used by the Rise margin calculator. | Market | Position notional | Max leverage | Position margin | | ------ | -------------------------------- | ------------ | --------------- | | BTC | $0.10 \times 62{,}000 = 6{,}200$ | $20\times$ | $310.000000$ | | SOL | $100 \times 140 = 14{,}000$ | $15\times$ | $933.333334$ | The account also has resting limit orders: | Market | Resting orders | Effect if filled | | ------ | -------------------------- | ------------------------------------------------------ | | ETH | Bid `2 ETH`, ask `1 ETH` | No current ETH position, so either side opens exposure | | SOL | Bid `50 SOL`, ask `25 SOL` | The bid reduces the short; the ask increases the short | Assume ETH mark price is `3,000` and SOL mark price is `140`. In the fallback parameters, ETH uses `20x` first-tier max leverage, SOL uses `15x`, and both markets use a `100%` first-tier `limit_order_risk_factor`. In the calculations below, $M$ denotes margin and $\Delta M$ denotes an increase in margin. For ETH: $$ M_{\text{bid, initial}} = \frac{2\ \text{ETH} \times 3{,}000}{20} = 300.000000 $$ $$ M_{\text{bid, order}} = 300.000000 \times 100\% = 300.000000 $$ $$ M_{\text{ask, initial}} = \frac{1\ \text{ETH} \times 3{,}000}{20} = 150.000000 $$ $$ M_{\text{ask, order}} = 150.000000 \times 100\% = 150.000000 $$ $$ M_{\text{ETH, initial}} = 0 + \max\left(300.000000, 150.000000\right) = 300.000000 $$ For SOL: $$ M_{\text{SOL, position}} = \frac{100\ \text{SOL} \times 140}{15} = 933.333334 $$ Buying $50\ \text{SOL}$ reduces the short from $100\ \text{SOL}$ to $50\ \text{SOL}$, so: $$ M_{\text{bid, order}} = 0 $$ Selling $25\ \text{SOL}$ increases the short from $100\ \text{SOL}$ to $125\ \text{SOL}$, so: $$ \begin{aligned} M_{\text{post-ask}} &= \frac{125\ \text{SOL} \times 140}{15} = 1{,}166.666667 \\[0.75em] \Delta M_{\text{ask}} &= 1{,}166.666667 - 933.333334 = 233.333333 \\[0.75em] M_{\text{ask, order}} &= 233.333333 \times 100\% = 233.333333 \end{aligned} $$ $$ M_{\text{SOL, initial}} = 933.333334 + \max\left(0, 233.333333\right) = 1{,}166.666667 $$ Total initial margin: $$ \begin{aligned} \text{BTC margin} &= 310.000000 \\[0.5em] \text{SOL margin} &= 1{,}166.666667 \\[0.5em] \text{ETH margin} &= 300.000000 \\[1em] \text{total initial margin} &= 1{,}776.666667\ \text{USDC} \end{aligned} $$ The account is healthy because effective collateral is above initial margin: $$ 13{,}150.000000\ \text{effective collateral} > 1{,}776.666667\ \text{initial margin} $$ Using the fallback threshold factors for these markets, the account's risk thresholds would be: | Threshold | Calculation | Result | | -------------------- | ------------------------------------------------------------------------------ | ---------------- | | Cancel margin | $310.000000 \times 70\% + 1{,}166.666667 \times 75\% + 300.000000 \times 70\%$ | $1{,}302.000000$ | | Maintenance margin | $\text{total initial margin} \times 50\%$ | $888.333333$ | | Backstop requirement | $\text{total initial margin} \times 20\%$ | $355.333333$ | | High-risk margin | $\text{total initial margin} \times 10\%$ | $177.666666$ | ## Maintenance margin and other thresholds Phoenix derives lower risk thresholds from initial margin using market-specific risk factors. These thresholds are the escalation ladder the protocol uses as an account becomes more dangerous to the system. $$ \begin{aligned} M_{\text{cancel}} &= M_{\text{initial}} \times f_{\text{cancel}} \\[0.5em] M_{\text{maintenance}} &= M_{\text{initial}} \times f_{\text{maintenance}} \\[0.5em] M_{\text{backstop}} &= M_{\text{initial}} \times f_{\text{backstop}} \\[0.5em] M_{\text{high risk}} &= M_{\text{initial}} \times f_{\text{high risk}} \end{aligned} $$ Here, $M$ is the margin threshold and $f$ is its corresponding market-specific risk factor. Initial margin is the opening and healthy-account requirement. Maintenance margin is the main liquidation threshold. If effective collateral falls below maintenance margin, the account can be liquidated. The broader risk sequence is: * below cancel margin: risk-increasing limit orders can be cancelled * below maintenance margin: market liquidation can begin * below backstop requirement: distressed positions can be transferred to a backstop account * below high-risk margin: ADL can become available See [Liquidations](/phoenix/margin-and-risk/liquidations) for the liquidation sequence. ## Liquidation estimates Liquidation price is an estimate, not a fixed promise. For a single position, Phoenix solves for the mark price where effective collateral would fall to the relevant maintenance requirement: $$ C + U_{\text{other}} + q\left(P - P_{\text{entry}}\right) = M_{\text{other}} + \frac{f_{\text{maintenance}}\left|q\right|P}{L} $$ Where: * $C$ is collateral * $U_{\text{other}}$ is the unrealized PnL contribution from other markets in the same cross account * $q$ is signed position size * $P$ is the estimated liquidation price * $L$ is the active leverage tier * $M_{\text{other}}$ is the maintenance margin from other markets in the same cross account * $f_{\text{maintenance}}$ is the maintenance risk factor This is why cross-margin liquidation prices can move when funding, mark prices, other positions, or resting orders change. In a cross account, profitable positions in one market can support risk in another market. That is capital efficient, but it also means liquidation estimates are portfolio-aware. A BTC liquidation price can move because SOL PnL changed, funding settled, or a risk-increasing ETH order was placed. SOL collateral adds another portfolio input. Its index value can change your liquidation estimate even when you did not trade the market being estimated. When you use SOL both as collateral and as the underlying asset of a SOL perpetual position, a change in the SOL price affects both the value of your collateral and the unrealized PnL of your SOL position. See [Liquidations](/phoenix/margin-and-risk/liquidations) for the expanded formula and limit-order example. # Mark Price Source: https://docs.phoenix.trade/phoenix/margin-and-risk/mark-price Mark price determines position valuations for PnL and liquidation calculations. ## Calculation Mark price is calculated as the median of three components: 1. **Adjusted Oracle Price:** Spot oracle price adjusted with a smoothed basis 2. **Book Price:** Median of best bid (highest price someone is willing to pay), best ask (lowest price someone is willing to sell at), and last trade on the Phoenix orderbook (most recent completed transaction price) 3. **Exchange Price:** Weighted median of perpetual prices from major external exchanges ## Design Goals Phoenix was designed with the following core principles: * Aim to closely track spot price movements for fair PnL and valuations * Help protect against manipulation or distortions from temporary liquidity shocks on individual venues * Seek to reduce the risk of unnecessary liquidation wicks due to short-term anomalies or low-liquidity events such as volatile market conditions, oracle delays, or network issues that may cause deviations # Risk Warning Source: https://docs.phoenix.trade/phoenix/margin-and-risk/risk-warning Important risk disclosures ## Overview Trading perpetual futures involves substantial risk of loss. Always monitor your maintenance margin and account health to avoid liquidation. Use appropriate position sizing and stop losses, especially when using leverage. ## Smart Contract Risks All smart contracts carry inherent risks. Vulnerabilities or exploits could result in loss of funds. ## Solana Network Risks Network congestion can delay transactions, order execution, liquidations, and funding settlements. Solana network outages may temporarily prevent access to positions. ## Oracle Dependency Risks Oracle price feeds can experience delays, gaps, or failures at times. Oracle outages could trigger unexpected liquidations or prevent timely position adjustments. # Phoenix Market Parameters Source: https://docs.phoenix.trade/phoenix/market-parameters Phoenix Markets: Risk, Leverage, and Parameters # Fees Source: https://docs.phoenix.trade/phoenix/matching-engine/fees Trading fees, liquidation fees, referral discounts, and Flight builder fees Phoenix charges trading fees on matched notional. ## Base trading fees * taker fee: $3.5\ \text{bps}$ ($0.035\%$) * maker fee: $0.5\ \text{bps}$ ($0.005\%$) Examples: $$ \begin{aligned} \text{taker fee} &= 10{,}000 \times 0.035\% = 3.50\ \text{USDC} \\[0.5em] \text{maker fee} &= 10{,}000 \times 0.005\% = 0.50\ \text{USDC} \end{aligned} $$ ## Maker vs taker * you are a maker when your order provides resting liquidity * you are a taker when your order removes existing liquidity A limit order can be either: * taker on the crossed portion * maker on any remainder that rests ## What else can affect your all-in cost ### Referral discount Referral codes can reduce user trading fees. See [Referral Program](/incentive-programs/referral-program). ### Flight builder fee If you route an order through Flight, the builder fee is additive to exchange trading fees. See [Builder Codes](/builder-codes). # FIFO Order Book Source: https://docs.phoenix.trade/phoenix/matching-engine/fifo-order-book Price-time priority and user-facing orderbook behavior Phoenix uses a FIFO order book for normal resting orders. FIFO means: * better price fills first * at the same price, older resting liquidity fills before newer resting liquidity ## What a resting order stores A resting FIFO order stores: * the maker trader position * initial and remaining base size * order flags * optional conditional order index * optional slot-based expiry * initial slot ## Capacity and order limits Phoenix enforces both per-trader open-order limits and market-level orderbook capacity. * A trader can have up to `64` resting bid limit orders per market. * A trader can have up to `64` resting ask limit orders per market. * The per-trader limit is per market and per side, so one trader can have up to `128` resting FIFO orders on one market: `64` bids and `64` asks. * IOC and other take-only orders do not count toward this cap because they do not rest on the book. ## Order packet families At the protocol layer, the core order packet kinds are: | Packet kind | User-facing mental model | Rests on book | | ------------------- | --------------------------------------- | -------------------------------- | | `PostOnly` | maker-only limit order | Yes | | `Limit` | standard limit order | Yes, after any immediate matches | | `ImmediateOrCancel` | IOC, market, or FOK depending on fields | No | ## Important fields ### `price_in_ticks` The order price in native book ticks. ### `num_base_lots` The total base size to fill or rest. ### `last_valid_slot` Optional expiry slot for the order. ### `match_limit` Optional cap on how many resting orders an incoming order can match against. ### `cancel_existing` Advanced behavior that lets some order paths cancel conflicting resting orders instead of failing a margin check immediately. ## Maker vs taker behavior * A resting order that provides liquidity is maker flow. * An incoming aggressive order that removes resting liquidity is taker flow. * A standard limit order can act as either, depending on whether it crosses the book. ## Post-only behavior Post-only orders are for adding liquidity only. * If they would immediately trade, they do not execute as takers. * Depending on configuration, they may slide to a valid resting price rather than cross. ## Expiry and live L2 views FIFO order expiry is slot-based. An expired order can remain in the raw book until a matching or maintenance path touches it. Consumers that need a live L2 view should filter expired FIFO orders before aggregating by price. # Matching Engine Source: https://docs.phoenix.trade/phoenix/matching-engine/matching-engine How Phoenix matches orders across the FIFO order book and spline liquidity Phoenix builds one matching view from two liquidity sources. ## Liquidity surfaces ### FIFO order book The FIFO order book is the standard limit-order book. Orders fill by price first, then by time priority within the same price level. ### Spline liquidity Spline liquidity is virtual liquidity published by authorized spline traders. A spline trader quotes a mid price and bid/ask regions around that mid. The engine materializes those regions into visible price levels when it builds the combined book. For each interaction, the engine builds a combined view over both sources. It loads the FIFO bid and ask maps, loads eligible splines, applies spline risk caps and current-slot fill state, and then matches against the best available price across the combined book. ## Fast quote updates Splines are designed to let market makers move liquidity with very low compute overhead. On a traditional FIFO book, a market maker that wants to move an entire quote ladder has to place and cancel many individual limit orders. A multi-limit-order transaction can cost up to roughly `300,000` compute units. With spline liquidity, the market maker can update the spline's reference price and move the quoted regions around that price. A spline oracle update costs around `600` compute units. That compute asymmetry matters for market structure. When external prices move, stale on-chain quotes are vulnerable to toxic taker flow. Lower-cost spline updates let market makers refresh quotes more quickly and more frequently, which helps them quote tighter spreads without pricing in as much stale-quote risk. This does not guarantee transaction ordering. It gives quote updates a much smaller compute footprint, which improves the economics and latency profile of keeping Phoenix liquidity close to the current market. Spline traders can also attach their own sequence numbers to updates, so stale updates can be rejected if validators reorder multiple spline updates from the same trader. ## Execution priority The engine uses price priority across FIFO and spline liquidity: * for an incoming buy, the lower ask fills first * for an incoming sell, the higher bid fills first * if FIFO and spline liquidity are available at the same price, spline liquidity fills first Equal-price spline priority does not let deeper spline liquidity skip better FIFO prices. When splines win priority at a price, the engine only matches spline levels up to the current FIFO boundary before returning to FIFO. ## High-level flow 1. A user submits an order packet. 2. The engine validates price, size, flags, expiry, and self-trade behavior. If the order can rest, it also checks the trader's per-side open-order cap before placement. 3. The engine builds the combined matching view. 4. The order matches eligible FIFO or spline liquidity until the order is filled, price-limited, budget-limited, or match-limited. 5. Any remaining size rests on the FIFO book, expires, or is discarded depending on the order packet. ## What changes fill behavior * whether the incoming order crosses the combined BBO * `self_trade_behavior` * `match_limit` * post-only slide or reject behavior * execution price band filtering * current spline risk counters * whether a spline-only cross has been resolved into slide prices # Order Types Source: https://docs.phoenix.trade/phoenix/matching-engine/order-types Phoenix order types, matching behavior, and advanced order controls Phoenix exposes a small set of core order packet families and then layers user-facing controls on top. ## Core order families | Public name | Protocol mental model | Rests on book | Notes | | --------------- | ---------------------------------------------- | ------------- | ----------------------------------------------- | | Market order | IOC with no explicit limit price | No | Pure taker flow | | IOC | Immediate-or-cancel with an optional price cap | No | Cancels any unfilled remainder | | FOK | IOC with minimum fill constraints | No | Entire minimum must fill or the order is voided | | Limit order | Cross if possible, then rest | Yes | Can be maker or taker depending on price | | Post-only order | Maker-only limit order | Yes | Will not take existing liquidity | ## User-facing modifiers ### Reduce only Reduce-only orders can only decrease existing exposure. ### Post only Post-only orders are used to add liquidity only. They do not execute immediately as takers. ### Take profit / stop loss These are trigger orders evaluated against mark price. See [Take Profit And Stop Loss](/phoenix/matching-engine/take-profit-stop-loss). ### Expiration Order packets can carry a `last_valid_slot`, which acts like an expiry. ### Match limit Advanced callers can set `match_limit` to cap how many resting FIFO orders or spline price levels an incoming order may consume. This is separate from the open-order cap. `match_limit` controls taker-side matching work for one incoming order. The open-order cap controls how many resting limit orders a trader may already have on one market side. For FIFO matching, each maker order consumes one `match_limit` unit. For spline matching, one spline price level consumes one `match_limit` unit even if multiple splines participate at that level. ### Cancel existing Some order flows can request cancellation of conflicting resting orders instead of failing a margin check immediately. ## How to think about order choice ### Use market / IOC when * speed matters most * you are willing to take available liquidity ### Use limit when * you need a price bound * you are okay with resting if not fully filled ### Use post-only when * you explicitly want maker behavior * you do not want an aggressive fill ## Self-trade behavior Phoenix also lets the matching engine choose how to handle self-crosses. At the protocol layer, the supported behaviors are: * `Abort` * `CancelProvide` * `DecrementTake` Standard limit and IOC builders default to `CancelProvide` unless a caller overrides it. # Self-Trade Prevention Source: https://docs.phoenix.trade/phoenix/matching-engine/self-trade-prevention How Phoenix handles orders that would match against your own liquidity Phoenix has explicit self-trade prevention modes at the matching-engine layer. ## Available behaviors | Behavior | What happens | | --------------- | --------------------------------------------------------------- | | `Abort` | Reject the self-cross instead of matching it | | `CancelProvide` | Cancel the resting provide-side order on the self-cross | | `DecrementTake` | Reduce the incoming take-side quantity by the self-cross amount | ## Default behavior For the core limit and IOC builders, the protocol defaults to `CancelProvide` unless the caller explicitly chooses another behavior. Some specialized flows use stricter defaults. For example: * many SDK examples use `Abort` * TP/SL-triggered orders are designed to avoid accidental self-matching ## Why this matters Without STP, a trader could: * fill against their own resting order * pay fees for a meaningless self-match * distort fill history and realized PnL ## User-facing outcomes If a self-cross is detected, you may see: * an order reject * a resting order cancellation * an incoming order that fills less than its original requested size Those are not random outcomes. They depend on the selected STP mode. # Spline Liquidity Source: https://docs.phoenix.trade/phoenix/matching-engine/spline-liquidity How spline traders quote and fill liquidity on Phoenix Spline liquidity is a Phoenix-specific liquidity source for quoting a shaped curve without placing one FIFO order at every price level. A spline trader publishes a mid price and bid/ask regions. The matching engine materializes those regions into visible price levels when it builds the combined book. ## Trader account restrictions A trader account with any active spline registration cannot place FIFO limit orders or stop-loss orders on any market. Use a separate trader account, such as a dedicated isolated subaccount, for spline activity if you also need regular order-book activity. Other trader accounts owned by the same wallet authority are unaffected. See [Accounts](/phoenix/collateral-and-accounts/accounts) for account boundaries. ## Spline data model A spline trader controls: | Control | Effect | | -------------------------- | ----------------------------------------------------------------------------------------- | | `mid_price` | Anchors every region in ticks. A zero mid price makes the spline ineligible for matching. | | bid and ask regions | Define the shape and size of liquidity below and above the mid. | | `refresh_regions` | Chooses whether an update preserves consumed liquidity or replenishes the regions. | | `max_position_size` | Optionally caps the absolute long and short positions the spline can reach. | | `leverage_decrease_in_bps` | Applies a collateral haircut when Phoenix calculates spline risk capacity. | Phoenix separately tracks sequence ordering, expiry, consumed visible and hidden liquidity, and risk-limited bid and ask capacity. Each region is defined by: * `start_offset`, inclusive * `end_offset`, exclusive * `density`, in base lots per tick * `top_level_hidden_take_size`, in base lots * `lifespan`, in slots Each side supports up to 10 regions. Regions on the same side must be ordered by increasing `start_offset` and cannot overlap; adjacent regions and gaps are allowed. A spline can quote only one side, but it must have at least one region in total. If both sides are configured, their first regions cannot both start at offset zero. Region offsets are tick distances from the spline mid: * bid offset $x$ maps to $\text{mid price} - x$ * ask offset $x$ maps to $\text{mid price} + x$ * region size is $(\text{end offset} - \text{start offset}) \times \text{density}$ base lots For example, a spline with mid `100`, bid region `[1, 3)`, and density `5` exposes: * `5` lots at `99` * `5` lots at `98` An ask region `[1, 3)` on the same spline exposes: * `5` lots at `101` * `5` lots at `102` ### Region lifespan `lifespan` is measured in slots from the spline's `user_update_slot`. A region remains eligible through `user_update_slot + lifespan`, inclusive. Every accepted price update resets the lifespan of every configured region by setting the spline's expiry anchor to that update's `user_update_slot`. Each region then receives its full configured lifespan from the new anchor. This happens whether `refresh_regions` is `true` or `false`; that flag controls consumed liquidity, not expiry. Use `GTC_LIFESPAN` (`u64::MAX`) for a region that does not expire. A lifespan of `0` is eligible only at `user_update_slot`; it does not mean good-til-cancelled. ## How spline levels become visible A spline level is eligible for the book when: * the spline is active * the spline has a nonzero mid price * the region has not expired * the region still has unfilled size * the spline trader has remaining risk capacity on that side The visible level is the first unfilled tick in the active region. If a tick is partially filled, only the remaining size at that tick is displayed. Once a tick is fully consumed, the next tick in the region becomes visible. Spline liquidity is therefore not a static list of orders. It is derived from the spline's mid price, region definitions, region fill state, expiry, and current risk counters. ## Updating a spline Both price and parameter updates accept a `refresh_regions` flag, but preserving state has different effects for the two update types: | Update | `refresh_regions=false` | `refresh_regions=true` | | ---------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Price | Moves the existing regions to the new mid while preserving their fill state, hidden-take fill state, and offsets. | Moves the regions and resets their visible fills, hidden fills, and offsets. | | Parameters | Replaces the regions, then reapplies the total consumed visible and hidden liquidity on each side to the new region shapes. | Replaces the regions with no consumed liquidity. | Previously consumed liquidity is replenished only when `refresh_regions=true`. ## Risk caps Before matching, Phoenix caps each spline's available bid and ask size using the spline trader's current risk state. The cap accounts for: * current margin and position state * `leverage_decrease_in_bps` * optional max position size limits * the current risk action After each spline fill, the in-memory risk counter is decremented. This prevents one instruction from consuming more spline liquidity than the trader can support across multiple price levels. `max_position_size` has two modes: | Value | Behavior | | ----------------------- | ----------------------------------------------------------------------------------- | | `Disabled` | Applies no spline-specific position cap. Normal margin and risk limits still apply. | | `Limit { long, short }` | Applies separate absolute long and short caps, both expressed in base lots. | `Limit { long: 0, short: 0 }` is reduce-only: the spline can quote only on the side that moves an existing position toward zero. When a limit is active, Phoenix clips each side to its remaining capacity: * bid capacity is capped by the distance from the current position to the maximum long position * ask capacity is capped by the distance from the current position to the maximum short position At the long cap, bid liquidity becomes zero while ask liquidity can remain available to reduce the position. At the short cap, the inverse applies. `leverage_decrease_in_bps` reduces the effective collateral used to calculate spline risk capacity: $$ \text{effective collateral for spline capacity} = \text{effective collateral} \times \frac{10{,}000 - \text{leverage decrease in bps}}{10{,}000} $$ The value must be between `0` and `10,000`. A value of `0` applies no haircut, `5,000` uses 50% of effective collateral, and `10,000` removes margin-backed capacity for increasing exposure. This setting does not change deposited collateral or the market's leverage tiers. ## Fees The spline trader pays zero exchange fees on spline fills: * when an incoming order matches a spline, the incoming trader pays its configured taker fee and the spline trader pays zero * during uncrossing, the resting FIFO trader pays its configured maker fee, or receives its configured maker rebate, while the spline trader pays zero See [Fees](/phoenix/matching-engine/fees) for standard maker and taker rates. ## Uncrossing and slide prices Spline liquidity from different traders can cross when their mids and regions overlap. Phoenix resolves this before rendering or matching spline-only liquidity. The uncross path: 1. Matches crossed splines against FIFO liquidity first when the spline crosses the explicit book. 2. Computes slide prices for remaining crossed spline-only liquidity. 3. Displays and matches more aggressive spline bids at the slide bid. 4. Displays and matches more aggressive spline asks at the slide ask. Slide prices make the remaining spline book non-crossing without writing synthetic orders into the FIFO book. Splines do not execute directly against other splines. A spline can execute against crossed FIFO liquidity during uncrossing; remaining spline-to-spline crossings are resolved using slide prices. ## Pro-rata fills At a single spline price level, FIFO price-time priority does not apply across spline traders. Phoenix allocates the fill pro-rata by available size: $$ \text{allocation} = \left\lfloor \frac{\text{available lots} \times \text{fill size}}{\text{total available}} \right\rfloor $$ Spline sequence number is still used for deterministic iteration and dust handling. It is not the primary priority rule. When integer rounding leaves unallocated lots, sequence order determines which spline receives the remainder. Important execution consequences: * one spline price level consumes one `match_limit` unit, even if multiple splines participate * one FIFO maker order consumes one `match_limit` unit * spline liquidity at the same price fills before FIFO liquidity * worse spline prices do not skip better FIFO prices ## Hidden take size `top_level_hidden_take_size` is configured per region in base lots. It is separate from the region's displayed density and total size, and it is not available to incoming takers. It is available only when the spline itself takes FIFO liquidity during uncrossing. In that path, Phoenix can consume visible size at the region's current top tick, then up to the remaining hidden take size at that same tick, then visible size through deeper ticks. Phoenix tracks consumed hidden size separately from visible fills, and `refresh_regions` applies to both. # Take Profit and Stop Loss Source: https://docs.phoenix.trade/phoenix/matching-engine/take-profit-stop-loss Attach conditional orders to positions or limit orders to close at a trigger price Take Profit (TP) and Stop Loss (SL) are conditional, reduce-only orders that execute when the market's mark price crosses a trigger price you specify. You can attach TP/SL to an open position or to a resting limit order. ## When triggers fire Triggers are evaluated against the market's [mark price](/phoenix/margin-and-risk/mark-price). | Position | Take Profit fires when | Stop Loss fires when | | -------- | ---------------------- | --------------------- | | Long | Mark price ≥ TP price | Mark price ≤ SL price | | Short | Mark price ≤ TP price | Mark price ≥ SL price | ## Execution style When a trigger fires, the conditional order is placed on the book in one of two modes: * **Market:** Default option. Submitted as an immediate-or-cancel (IOC), all-or-none (AON) order. By default, these have the following slippage tolerances: * Take Profit: 0.25% * Stop Loss: 10% You can edit these values in settings. * **Limit:** Submitted as a standard limit order at an execution price you specify. If your limit price doesn't cross the book, the order rests until it fills or is cancelled. During real-world asset internal pricing periods, TP/SL market order slippage and limit order execution prices are clamped to the price bound limits. TP/SL orders are always reduce-only. ## Where to place TP/SL ### On a new order From the order form, enable the TP/SL toggle to attach conditional legs to a market or limit order. You can set each leg by trigger price, by gain/loss percentage, or by dollar amount — the inputs stay in sync. These will be placed as TP/SL [market orders](#execution-style). ### On an open position Open the positions table and click the TP/SL cell for the position you want to bracket. The edit modal lets you set or update both legs independently. When triggered, the order executes for your full position size at that moment. ### On a resting limit order From the open orders table, open a limit order and attach TP/SL in the edit modal. Once attached, the TP and SL appear as child rows beneath the parent limit order and share the parent's size. ## Limit order TP/SL lifecycle A TP/SL attached to a limit order is linked to its parent while the parent rests on the book. * **Partial fill of the parent:** The attached TP/SL's fillable size updates to match the parent's filled amount. The trigger, if breached, executes up to the filled amount. * **Full fill of the parent:** The TP/SL detaches from the parent and becomes an **orphaned** conditional order. Orphaned orders remain active and can execute up to their full size when triggered. * **Parent cancelled:** All attached TP/SL children are cancelled with the parent. * **Cancel all:** Cancels all open orders, including attached children and orphans. You can edit or cancel a child TP/SL directly without touching the parent. ### Position flips Closing a position and opening the opposite side invalidates all active TP/SL on that market, including both position-based orders and any orphans or attached children. You'll need to reattach TP/SL after flipping. ## Chart controls TP/SL orders render as lines on the trading chart. You can edit or cancel them directly from the chart. ## Limitations * TP/SL cannot be attached to reduce-only limit orders. * Self-trade prevention applies on execution. * A trader account supports a bounded number of active conditional orders per market. # Perpetual Futures Source: https://docs.phoenix.trade/phoenix/perpetual-futures How Phoenix perpetual futures work, what makes them different from spot, and where each risk mechanism fits Perpetual futures are leveraged contracts that track an underlying asset without an expiry date. Phoenix uses them to let traders: * go long or short with posted collateral * trade against both resting limit orders and fast-updating spline liquidity * keep positions open indefinitely as long as margin stays healthy ## What makes a perp different from spot In spot trading, you buy or sell the asset itself. In perps, you trade synthetic exposure instead: * PnL is driven by mark-price changes versus your entry price * leverage is created by posting only a fraction of notional as collateral * funding transfers value between longs and shorts to keep the market anchored * liquidation rules enforce solvency when collateral is no longer sufficient ## The core moving parts ### Entry price and PnL Every position has an effective entry price and then accumulates unrealized PnL as mark price moves. See [Entry Price And PnL](/phoenix/margin-and-risk/entry-price-pnl). ### Mark price Phoenix uses a robust mark price instead of the last trade price. It combines an adjusted oracle price, Phoenix order book data, and external perpetual prices so risk checks are anchored to a fair market estimate rather than a single venue or transient fill See [Mark Price](/phoenix/margin-and-risk/mark-price). ### Funding Because perps have no expiry, funding is the mechanism that keeps the perp aligned with the underlying market over time. See [Funding Rate](/phoenix/margin-and-risk/funding-rate). ### Margin Margin lets traders take larger positions than their posted collateral alone would otherwise support, while accepting liquidation risk if losses, funding, or changing risk requirements erode the account’s collateral. See [Accounts](/phoenix/collateral-and-accounts/accounts) and [Margin Math](/phoenix/margin-and-risk/margin-math). ### Liquidations and ADL If effective collateral falls below maintenance margin, Phoenix first attempts normal liquidation through the order book after cancelling risk-increasing open orders. If the account remains underwater, liquidation can escalate to a backstop transfer, and finally ADL can be used last to close the remaining risk against profitable traders on the opposite side to preserve the exchange's health. See [Liquidations](/phoenix/margin-and-risk/liquidations). ## How to navigate the docs from here * Start with [Accounts](/phoenix/collateral-and-accounts/accounts) if you need the wallet and trader-account model * Read [Matching Engine](/phoenix/matching-engine/matching-engine) if you want to understand fills * Read [Margin Math](/phoenix/margin-and-risk/margin-math) if you want to understand liquidation risk * Use [Market Parameters](/phoenix/market-parameters) once the earlier concepts are familiar # Impact Pricing Source: https://docs.phoenix.trade/phoenix/real-world-assets/impact-pricing How Phoenix uses order book impact to price real-world asset perps when external markets are closed Impact pricing is Phoenix's internal pricing mechanism for real-world asset perp markets when external markets are closed. It uses Phoenix order book liquidity to move the last known external oracle price gradually, instead of freezing the index price until the next external session. ## Book Impact With previous oracle price $S$, Phoenix calculates the price impact of crossing a configured notional amount against the order book: $$ P_{\text{impact}} = \max\left(P_{\text{bid}} - S, 0\right) - \max\left(S - P_{\text{ask}}, 0\right) $$ where $P_{\text{bid}}$ and $P_{\text{ask}}$ are the average execution prices for selling and buying $1{,}000\ \text{USD}$ notional, respectively. The configured notional is currently $1{,}000\ \text{USD}$ and is subject to change. If one side of the book does not have enough depth for the configured notional, that side contributes `0` to the impact calculation. ## EMA Update Phoenix adds the book impact price to the previous oracle price to form a target price: $$ P_{\text{target}} = S + P_{\text{impact}} $$ The internal oracle then moves toward the target using a continuous-time EMA. The default EMA time constant is 1 hour. ## Bounds For real-world asset markets in internal pricing periods, Phoenix applies the market's configured price bounds before emitting the internal price. The bound is set around the last known external index price. For example, a market with 20x maximum leverage has a 5% bound, while a market with 25x maximum leverage has a 4% bound. # Index Price Source: https://docs.phoenix.trade/phoenix/real-world-assets/index-price How commodity and equity perp index prices update across external and internal pricing windows ## Oracle Prices Phoenix real-world asset markets are perpetual futures. The sections below describe index pricing for commodity perps and equity perps. Oracle prices for commodity perps and equity perps are denoted in USD. Phoenix does not apply a USDC/USD spot-price correction to these oracle prices. Phoenix uses a number of independent data providers for external real-world asset prices. ## Commodity Perps #### Market Hours Commodity perp oracle prices are externally sourced during the following market schedule: | Session | Time | Status | | ------------------ | ----------------------------------------------- | -------- | | Sunday open | 6:00 PM - 12:00 AM ET | External | | Monday - Thursday | 12:00 AM - 5:00 PM ET and 6:00 PM - 12:00 AM ET | External | | Daily market break | 5:00 PM - 6:00 PM ET | Internal | | Friday | 12:00 AM - 5:00 PM ET | External | During these market hours, commodity index prices are derived directly from liquid spot and futures markets. Market hours are subject to exchange holidays and early closes. #### After Hours When traditional commodity markets are closed, the index price updates using [Impact Pricing](/phoenix/real-world-assets/impact-pricing), Phoenix's internal order-book based pricing mechanism. #### Commodities Futures Rolling Some commodity perp markets derive their pricing from futures contracts during market hours. Because futures contracts expire, the index price handles "rolling" the pricing to the next active futures contract via a 5-day roll between the 5th and 10th business days of the month. During the roll period, 20% of the weight is shifted from the current contract to the next active contract at 5:30 ET every business day until the current contract is at 0% weight. Note that this happens during after hours, as markets are closed between 5 and 6 ET. Currently this applies to the WTIOIL and COPPER markets. | Business Day | Current Contract Weight | Next Active Contract Weight | | ------------ | ----------------------- | --------------------------- | | 5 | 100% | 0% | | 6 | 80% | 20% | | 7 | 60% | 40% | | 8 | 40% | 60% | | 9 | 20% | 80% | | 10 | 0% | 100% | ## Equity Perps #### Trading Hours Equity perp oracle prices are externally sourced 24/5, from Sunday 8:00 PM ET to Friday 8:00 PM ET. Phoenix achieves this by aggregating across the following equity-market sessions: | Session | Time | | ----------- | -------------------- | | Pre-market | 4:00 AM - 9:30 AM ET | | Market | 9:30 AM - 4:00 PM ET | | Post-market | 4:00 PM - 8:00 PM ET | | Overnight | 8:00 PM - 4:00 AM ET | #### Market Closed When the 24/5 external pricing window is closed, the index price updates using [Impact Pricing](/phoenix/real-world-assets/impact-pricing), Phoenix's internal order-book based pricing mechanism. #### Futures Rolling Equity markets do not use futures rolling mechanics. ## Transitions When transitioning from an internal pricing period to an external pricing window, the index price instantly snaps to the new external market price. When transitioning from an external pricing window to an internal pricing period, the last known external price becomes the base value for internal pricing. # Mark Price And Bounds Source: https://docs.phoenix.trade/phoenix/real-world-assets/mark-price-and-bounds How mark price and closed-period price bounds work for real-world asset perps ## Mark Price Commodity perps and equity perps use the same mark-price construction. Just as in traditional crypto assets, the Mark Price is calculated as the median of three components. 1. **Adjusted Oracle Price:** Spot oracle price adjusted with a smoothed basis 2. **Book Price:** Median of best bid, best ask, and last trade on the Phoenix orderbook 3. **Exchange Price:** The spot oracle price plus a 375 slot EMA of the difference between the book mid price and the spot oracle price. During internal pricing periods, the EMA is taken over 9000 slots. The third component differs from traditional crypto assets: it replaces the external perp exchange price. The oracle price can either be derived from external sources during external pricing windows or internal pricing during closed periods. ## Price Bounds During internal pricing periods, price movements on Phoenix are capped within a range to protect users against oracle price manipulation in lower liquidity hours. The cap is set 1/(max leverage) away from the last known external market price. For example, a market with 20x maximum leverage has a 5% bound, while a market with 25x maximum leverage has a 4% bound. The matching engine does not allow trades to execute outside of the price bounds. # Market Specs Source: https://docs.phoenix.trade/phoenix/real-world-assets/market-specs Leverage, underlying references, price bounds, and margin modes for commodity and equity perps For open interest caps and the full live parameter set, see [Market Parameters](/phoenix/market-parameters). ## Commodity Perps | Market | Leverage | Closed-Period Price Bound | Underlying | Margin | | :----- | :------- | :------------------------ | :--------- | :------- | | GOLD | 25x | 4% | XAU/USD | Isolated | | SILVER | 25x | 4% | XAG/USD | Isolated | | COPPER | 20x | 5% | HG/USD | Isolated | | WTIOIL | 20x | 5% | M6/USD | Isolated | ## Equity Perps | Market | Leverage | Closed-Period Price Bound | Underlying | Margin | | :----- | :------- | :------------------------ | :--------- | :------- | | AAPL | 20x | 5% | AAPL/USD | Isolated | | AMD | 10x | 10% | AMD/USD | Cross | | AMZN | 20x | 5% | AMZN/USD | Cross | | CRWV | 10x | 10% | CRWV/USD | Cross | | GOOGL | 20x | 5% | GOOGL/USD | Cross | | INTC | 10x | 10% | INTC/USD | Cross | | META | 20x | 5% | META/USD | Cross | | MSFT | 20x | 5% | MSFT/USD | Cross | | MU | 15x | 6.67% | MU/USD | Cross | | NVDA | 20x | 5% | NVDA/USD | Isolated | | SNDK | 15x | 6.67% | SNDK/USD | Cross | | SPCX | 15x | 6.67% | SPCX/USD | Isolated | | TSLA | 20x | 5% | TSLA/USD | Cross | # Accounts Source: https://docs.phoenix.trade/sdk/accounts Manage Phoenix trader account indexes, cross-margin accounts, isolated subaccounts, and trader-state subscriptions with the Rise SDK. This page covers the trader account model after a trader is eligible to use Phoenix. To activate a trader account, see [Trader Onboarding](/sdk/register). ## Trader account indexes A Phoenix trader account is a PDA derived from the authority wallet, `pda_index`, `subaccount_index`, and the Phoenix program id. In the TypeScript SDK these are passed as `traderPdaIndex` and `traderSubaccountIndex`; in Rust, `TraderKey::new(...)` uses `(pda_index = 0, subaccount_index = 0)` and `TraderKey::new_with_idx(...)` lets you specify both indexes. Use `pda_index = 0` for user accounts. With exchange gating enabled, only `pda_index = 0` can be activated; a non-zero PDA index cannot be activated. In the future this will be used to support multiple portfolios. * `subaccount_index = 0` is the cross-margin account. It is created during onboarding with `max_positions` between `32` and `128` inclusive; the default is `128`. * `subaccount_index > 0` is an isolated account. Isolated accounts have `max_positions = 1`, so each account is intended for one isolated position. ## Cross vs isolated Cross margin and isolated margin are represented as separate trader subaccounts under the same authority and `pda_index`. | Account | Index | Created by | Intended use | State stream behavior | | ---------------- | ---------------------- | ------------------------------------------------ | ---------------------------------------------- | ----------------------------------------------------- | | Cross account | `subaccount_index = 0` | Trader onboarding | Shared collateral and many positions | Included in the trader-state snapshot and deltas | | Isolated account | `subaccount_index > 0` | Register child subaccount, then sync from parent | One isolated position with separate collateral | Included in the same trader-state snapshot and deltas | Subscribe to trader state once per authority and `traderPdaIndex`. The stream returns the cross account and every registered isolated subaccount in the same snapshot, then sends deltas keyed by `subaccountIndex`. ## Trader state across subaccounts Use the SDK state primitives instead of applying raw snapshot and delta messages yourself. In TypeScript, `createTraderStateStore(client)` exposes `subaccountIndices()`, `subaccount(index)`, `position(index, symbol)`, `orders(index, symbol)`, `triggers(index, symbol)`, and `marginInputs()` across all subaccounts. In Rust, `Trader::apply_update(&msg)` maintains a `Trader` container whose `subaccounts` map includes cross and isolated accounts. ```ts TypeScript theme={null} import { createPhoenixClient, createTraderStateStore } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", ws: { connectMode: "eager" }, exchangeMetadata: { stream: true }, }); const traderState = createTraderStateStore(client); const resource = traderState.resource({ authority: "AUTHORITY_PUBKEY", traderPdaIndex: 0, }); const release = resource.retain(); const snapshot = await resource.ready(); for (const subaccount of snapshot?.subaccounts ?? []) { console.log(subaccount.subaccountIndex, subaccount.collateral); } const unsubscribe = resource.subscribe((state, previous) => { if (state.snapshot === previous.snapshot) return; for (const subaccountIndex of resource.subaccountIndices()) { const subaccount = resource.subaccount(subaccountIndex); console.log({ subaccountIndex, collateral: subaccount?.collateral, positions: subaccount?.positionSymbols, orderSymbols: subaccount?.orderSymbols, }); } }); // Later, when this component/service no longer needs live state: unsubscribe(); release(); ``` ```rust Rust theme={null} use phoenix_rise::api::{PhoenixWSClient, Trader, TraderKey}; use solana_pubkey::Pubkey; let authority: Pubkey = "AUTHORITY_PUBKEY".parse()?; let key = TraderKey::from_authority(authority); let mut trader = Trader::new(key.clone()); let ws = PhoenixWSClient::new_from_env()?; let (mut rx, _handle) = ws.subscribe_to_trader_state(&authority)?; while let Some(msg) = rx.recv().await { trader.apply_update(&msg); for (subaccount_index, subaccount) in &trader.subaccounts { println!( "subaccount={} collateral={} positions={} orders={}", subaccount_index, subaccount.collateral, subaccount.positions.len(), subaccount.orders.len() ); } } ``` References: * [TypeScript trader-state store example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/04-trader-state-store.ts) * [Rust trader-state example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/subscribe_trader_state.rs) ## Set position authority Use the on-chain `DelegateTrader` instruction to set or replace the `position_authority` for a trader account. The current account `authority` signs the transaction; the new position authority is recorded on the trader account but does not need to sign the delegation transaction. Set `position_authority` to an embedded wallet, such as a Privy wallet, when you want your app to support one-click transaction signing for trading actions while keeping collateral withdrawals controlled by the user's authority wallet. The SDK parameter `traderPdaIndex` corresponds to the account's `portfolio_index`, and `traderSubaccountIndex` corresponds to `subaccount_index`. ```ts TypeScript theme={null} import { createPhoenixClient } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", exchangeMetadata: { stream: false }, }); const authority = "PHANTOM_WALLET_PUBKEY"; const newPositionAuthority = "PRIVY_WALLET_PUBKEY"; const delegateTraderIx = await client.ixs.buildDelegateTrader({ traderWallet: authority, traderPdaIndex: 0, traderSubaccountIndex: 0, newPositionAuthority, }); // Send this instruction in a transaction signed by `authority`. ``` `DelegateTrader` updates one trader account. If you use isolated accounts, each isolated trader account has its own stored `position_authority`. Syncing a child isolated account from its parent cross account copies the parent's current `position_authority` to the child. ### Send market orders with position authority When a trader account has a separate `position_authority`, build order instructions with the trader account's original `authority`, but make the position authority the signing wallet. | SDK | Trader account owner | Market-order signer | | ---------- | ---------------------------------------------------- | ---------------------------------------------------------------- | | TypeScript | Pass `authority` as the trader account authority | Pass `positionAuthority` separately | | Rust | Derive `TraderKey` from the trader account authority | Pass `position_authority` to `MarketOrderTicket::authority(...)` | ```ts TypeScript theme={null} import { Side } from "@ellipsis-labs/rise"; const authority = "PHANTOM_WALLET_PUBKEY"; const positionAuthority = "PRIVY_WALLET_PUBKEY"; const symbol = "SOL"; const marketPacket = await client.orderPackets.buildMarketOrderPacket({ symbol, side: Side.Bid, baseUnits: "0.25", }); const placeMarketIx = await client.ixs.placeMarketOrder({ authority, positionAuthority, symbol, orderPacket: marketPacket, }); // Send the transaction with `positionAuthority` as the signer. ``` ```rust Rust theme={null} use phoenix_rise::{MarketOrderTicket, PhoenixTxBuilder, Side, TraderKey}; let authority = phantom_wallet_pubkey; let position_authority = privy_wallet_pubkey; let trader = TraderKey::new(authority); let builder = PhoenixTxBuilder::new(&metadata); let market_ixs = builder .place_market_order( MarketOrderTicket::builder() .authority(position_authority) .trader_account(trader.pda()) .symbol("SOL") .side(Side::Bid) .num_base_lots(25_000) .build()?, ) .await?; // Send the transaction with `position_authority` as the signer. ``` In TypeScript, `authority` is still used to resolve the trader PDA, and `positionAuthority` becomes the signer account in the Phoenix instruction. In Rust, `TraderKey::new(authority)` still resolves the trader PDA from the original wallet authority, but the ticket's `.authority(position_authority)` is the signer placed into the market-order instruction. Do not derive a new trader account from the position authority. The trader account remains the PDA for the user's authority wallet; only the signer changes. ## Create isolated accounts Onboarding creates the user's cross-margin trader account at `traderPdaIndex = 0` and `traderSubaccountIndex = 0`. After that cross account exists, create isolated accounts by registering a non-zero subaccount index under `traderPdaIndex = 0`, then syncing the parent cross account to the child isolated account. The sync instruction copies the parent account's current capabilities and fee configuration to the isolated account. ```ts TypeScript theme={null} import { MarginType, createPhoenixClient } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", exchangeMetadata: { stream: false }, }); const authority = "AUTHORITY_PUBKEY"; const traderPdaIndex = 0; const isolatedSubaccountIndex = 1; const registerIsolatedIx = await client.ixs.buildRegisterTrader({ authority, marginType: MarginType.Isolated, traderPdaIndex, traderSubaccountIndex: isolatedSubaccountIndex, }); const syncIsolatedIx = await client.ixs.buildSyncParentToChild({ traderWallet: authority, traderPdaIndex, traderSubaccountIndex: isolatedSubaccountIndex, }); const instructions = [registerIsolatedIx, syncIsolatedIx]; // Send the instructions in a transaction signed by `authority`. // The parent cross account must already exist. ``` ```rust Rust theme={null} use phoenix_rise::{PhoenixHttpClient, PhoenixMetadata, PhoenixTxBuilder, TraderKey}; use solana_keypair::read_keypair_file; use solana_signer::Signer; let keypair = read_keypair_file("PATH_TO_KEYPAIR.json")?; let parent_key = TraderKey::new(keypair.pubkey()); // pda_index 0, subaccount_index 0 let child_key = TraderKey::new_with_idx(parent_key.authority(), 0, 1); let http = PhoenixHttpClient::new_from_env()?; let exchange = http.get_exchange().await?.into(); let metadata = PhoenixMetadata::new(exchange); let builder = PhoenixTxBuilder::new(&metadata); let mut instructions = builder.build_register_trader( child_key.authority(), child_key.pda_index, child_key.subaccount_index, )?; instructions.extend(builder.build_sync_parent_to_child( child_key.authority(), parent_key.pda(), child_key.pda(), )?); // Send the instructions in a transaction signed by `authority`. // The parent cross account must already exist. ``` Use `traderSubaccountIndex = 0` only for the cross account. Isolated accounts use non-zero subaccount indexes. ## Fund isolated accounts and open positions When you open an isolated position yourself, mirror the same instruction order used by the server-built isolated order routes: 1. Pick a non-zero isolated subaccount index. 2. If that child account does not exist, register it. 3. Sync the parent cross account to the child account. 4. Transfer collateral from the parent cross account to the child account with `TransferCollateral`. 5. Place the order on the child account. 6. Optionally append `TransferCollateralChildToParent` to sweep child collateral back to the parent when the child has no active limit orders or positions, including when the order you just placed closes the isolated position. The parent cross account is subaccount `0`. `TransferCollateral` moves a specific amount between two trader accounts; use `srcSubaccountIndex = 0` and `dstSubaccountIndex = childSubaccountIndex` to fund an isolated child. TypeScript amounts are native USDC units, so `1 USDC = 1_000_000n`. The Rust builder accepts decimal USDC amounts. Unused isolated collateral does not stay parked forever. The server-built isolated order routes append `TransferCollateralChildToParent` by default, which sweeps collateral back to cross margin in the same transaction if the order closes the position and leaves no active limit orders. An off-chain crank can also sweep idle isolated collateral when the subaccount has no active limit orders or positions. ```ts TypeScript theme={null} import { MarginType, OrderFlags, SelfTradeBehavior, Side, createPhoenixClient, } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", exchangeMetadata: { stream: false }, }); const authority = "AUTHORITY_PUBKEY"; const traderPdaIndex = 0; const childSubaccountIndex = 1; const symbol = "SOL"; const usdc = (wholeUsdc: bigint) => wholeUsdc * 1_000_000n; const traderSnapshot = await client.api .traders() .getTraderStateSnapshot(authority, { traderPdaIndex }); const childExists = traderSnapshot.snapshot.subaccounts.some( (subaccount) => subaccount.subaccountIndex === childSubaccountIndex ); const instructions = []; if (!childExists) { instructions.push( await client.ixs.buildRegisterTrader({ authority, marginType: MarginType.Isolated, traderPdaIndex, traderSubaccountIndex: childSubaccountIndex, }) ); } instructions.push( await client.ixs.buildSyncParentToChild({ traderWallet: authority, traderPdaIndex, traderSubaccountIndex: childSubaccountIndex, }) ); instructions.push( await client.ixs.buildTransferCollateral({ authority, traderPdaIndex, srcSubaccountIndex: 0, dstSubaccountIndex: childSubaccountIndex, amount: usdc(100n), }) ); const orderPacket = await client.ixs.orderPackets.buildLimitOrderPacket({ symbol, side: Side.Bid, priceUsd: "150", baseUnits: "1", selfTradeBehavior: SelfTradeBehavior.CancelProvide, orderFlags: OrderFlags.None, cancelExisting: false, }); instructions.push( await client.ixs.buildPlaceLimitOrder({ authority, symbol, orderPacket, traderPdaIndex, traderSubaccountIndex: childSubaccountIndex, }) ); // Optional: mimics the server isolated-order routes' default cleanup step. // If this order closes the isolated position and leaves no resting orders, // the remaining child collateral is moved back to the cross account. instructions.push( await client.ixs.buildTransferCollateralChildToParent({ authority, traderPdaIndex, childSubaccountIndex, }) ); // Send the instructions in a transaction signed by `authority`. // If you use `positionAuthority`, pass it to transfer/order builders and // sign the relevant instructions with that delegated authority. ``` ```rust Rust theme={null} use phoenix_rise::{ LimitOrderTicket, PhoenixHttpClient, PhoenixMetadata, PhoenixTxBuilder, Side, TraderKey, }; use solana_keypair::read_keypair_file; use solana_signer::Signer; let keypair = read_keypair_file("PATH_TO_KEYPAIR.json")?; let parent_key = TraderKey::new(keypair.pubkey()); // pda_index 0, subaccount_index 0 let child_key = TraderKey::new_with_idx(parent_key.authority(), 0, 1); let http = PhoenixHttpClient::new_from_env()?; let exchange = http.get_exchange().await?.into(); let metadata = PhoenixMetadata::new(exchange); let builder = PhoenixTxBuilder::new(&metadata); let child_exists = http .traders() .get_trader_subaccount( &parent_key.authority(), child_key.pda_index, child_key.subaccount_index, ) .await? .is_some(); let mut instructions = Vec::new(); if !child_exists { instructions.extend(builder.build_register_trader( child_key.authority(), child_key.pda_index, child_key.subaccount_index, )?); } instructions.extend(builder.build_sync_parent_to_child( child_key.authority(), parent_key.pda(), child_key.pda(), )?); instructions.extend(builder.build_transfer_collateral( child_key.authority(), parent_key.pda(), child_key.pda(), 100.0, )?); let order_ticket = LimitOrderTicket::builder() .authority(child_key.authority()) .trader_account(child_key.pda()) .symbol("SOL") .side(Side::Bid) .price(150.0) .num_base_lots(1) .subaccount_index(child_key.subaccount_index) .build()?; instructions.extend(builder.place_limit_order(order_ticket).await?); // Optional: mimics the server isolated-order routes' default cleanup step. // If this order closes the isolated position and leaves no resting orders, // the remaining child collateral is moved back to the cross account. instructions.extend(builder.build_transfer_collateral_child_to_parent( child_key.authority(), child_key.pda(), parent_key.pda(), )?); // Send the instructions in a transaction signed by `authority`. ``` The server routes also check that the parent has enough transferable collateral before creating the transfer instruction. If your app builds these instructions client-side, perform the same check from trader state so you do not try to move collateral reserved for existing margin requirements. ## Return isolated collateral to cross margin Use `TransferCollateralChildToParent` when an isolated child account should return its collateral to the parent cross account. This instruction targets one child subaccount and transfers all available collateral back to subaccount `0`; it does not take an amount parameter. The child account must have no active limit orders or positions. If the child account has no collateral left to transfer, the instruction no-ops on-chain. This is useful after a close-position order. Place the close order first, then append `TransferCollateralChildToParent`; if the close leaves the isolated account with no active position and no active limit orders, the remaining collateral is moved back to the cross account. ```ts TypeScript theme={null} const authority = "AUTHORITY_PUBKEY"; const traderPdaIndex = 0; const isolatedSubaccountIndex = 1; const transferToParentIx = await client.ixs.buildTransferCollateralChildToParent({ authority, traderPdaIndex, childSubaccountIndex: isolatedSubaccountIndex, }); // Send the instruction in a transaction signed by `authority`. // The parent cross account and child isolated account must already exist. ``` ```rust Rust theme={null} let parent_key = TraderKey::new(keypair.pubkey()); // pda_index 0, subaccount_index 0 let child_key = TraderKey::new_with_idx(parent_key.authority(), 0, 1); let transfer_ixs = builder.build_transfer_collateral_child_to_parent( child_key.authority(), child_key.pda(), parent_key.pda(), )?; // Send the instructions in a transaction signed by `authority`. // The parent cross account and child isolated account must already exist. ``` # Auth Source: https://docs.phoenix.trade/sdk/auth JWT lifecycle for authenticated Phoenix API and Rise SDK sessions. Phoenix auth issues short-lived access JWTs and longer-lived refresh tokens. The Rise SDK can manage the session, attach bearer tokens to authenticated requests, refresh before expiry, and persist the rotated token pair. Most Phoenix market and trader read routes are public. You can fetch exchange params, market stats, orderbooks, candles, and trader state without first creating a session. Use auth when your integration needs an identified caller, higher API limits for authenticated traffic, or authenticated workflows. Auth is required for referral-code onboarding with `/v1/referral/activate-tx` and for notifications. Auth does not replace Solana transaction signing. Phoenix instructions still need the correct wallet, authority, position authority, or fee payer signature depending on the instruction. ## Lifecycle 1. Request a challenge from Phoenix. 2. Sign the challenge with the authority wallet. 3. Exchange the signature for an auth response. 4. Use `Authorization: Bearer ` on authenticated routes. 5. Refresh with `POST /v1/auth/refresh` before the access token expires. 6. Reauthenticate when refresh fails with a terminal auth error. The auth response shape is: ```json theme={null} { "token_type": "Bearer", "access_token": "eyJ...", "expires_in": 900, "refresh_token": "eyJ...", "refresh_expires_in": 2592000 } ``` Store the full response. Refresh rotates the access token and refresh token; the previous access token is accepted for a short grace window. ## Endpoints | Step | Endpoint | Body or query | Notes | | ---------------------------- | -------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------- | | Wallet nonce | `GET /v1/auth/nonce?wallet_pubkey=...` | query `wallet_pubkey` | Returns `nonce_id`, `message`, and `expires_at`. | | Wallet login | `POST /v1/auth/login/wallet` | `wallet_pubkey`, `signature`, `nonce_id` | `signature` signs the exact nonce `message`. | | Wallet transaction challenge | `POST /v1/auth/wallet/transaction-challenge` | `wallet_pubkey` | Alternative for wallets that cannot sign arbitrary messages. | | Wallet transaction login | `POST /v1/auth/login/wallet/transaction` | `wallet_pubkey`, `nonce_id`, `signed_transaction` | Exchanges the signed memo transaction for JWTs. | | Refresh | `POST /v1/auth/refresh` | `refresh_token` | SDKs include the current bearer token when available and store the rotated session. | | Logout | `POST /v1/auth/logout` | none | Requires `Authorization: Bearer ` and revokes the session. | Routes that can benefit from or require auth: * Referral-code onboarding: see [Trader Onboarding](/sdk/register#with-a-referral-code). * Notifications over REST or WebSocket. * Any protected account, user, or builder workflow marked with bearer auth in the [API reference](/api). * Authenticated integrations that need higher rate limits than anonymous public traffic. ## Wallet login This TypeScript example is written for a browser wallet that supports `signMessage`. ```ts TypeScript theme={null} import { LocalStorageAuthSessionStorage, createPhoenixClient, } from "@ellipsis-labs/rise"; import bs58 from "bs58"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", auth: true, authConfig: { storage: new LocalStorageAuthSessionStorage(), }, ws: false, }); const auth = client.auth!; const walletPubkey = wallet.publicKey.toString(); const nonce = await auth.getWalletNonce(walletPubkey); const message = new TextEncoder().encode(nonce.message); const signature = await wallet.signMessage(message); const session = await auth.loginWithWalletSignature( walletPubkey, bs58.encode(signature), nonce.nonce_id ); console.log(session.accessToken); ``` ```rust Rust theme={null} use phoenix_rise::PhoenixHttpClient; use solana_keypair::Keypair; use solana_signer::Signer; let client = PhoenixHttpClient::builder("https://perp-api.phoenix.trade") .enable_auth() .build()?; let auth = client.auth().expect("auth enabled"); let wallet = Keypair::new(); // Load the user's wallet in production. let wallet_pubkey = wallet.pubkey().to_string(); let nonce = auth.get_wallet_nonce(&wallet_pubkey).await?; let signature = wallet.sign_message(nonce.message.as_bytes()); let session = auth .login_with_wallet_signature( &wallet_pubkey, signature.as_ref(), nonce.nonce_id, ) .await?; println!("{}", session.access_token()); ``` In Rust builds with the `solana-keypair` feature, `auth.login_with_wallet_keypair(&keypair).await?` wraps the nonce, signing, and login sequence. ## Refresh With a managed SDK session, refresh is normally automatic before authenticated HTTP and WebSocket requests. Manual refresh is still useful when you want to force rotation after loading a saved session. ```ts TypeScript theme={null} const auth = client.auth!; const manager = client.sessionManager!; const current = await manager.getSession(); if (!current) { throw new Error("No Phoenix auth session"); } const refreshed = await auth.refresh(current.refreshToken); console.log(refreshed.accessToken); ``` ```rust Rust theme={null} let auth = client.auth().expect("auth enabled"); let refreshed = auth.refresh_session().await?; println!("{}", refreshed.access_token()); ``` Terminal refresh failures mean the user must sign in again: * `invalid_refresh_token` * `refresh_expired` * `session_missing` ## Auth errors Auth errors use the standard API error shape: ```json theme={null} { "error": "invalid_access_token" } ``` Common unauthenticated or reauthentication cases: | Error code | Status | Meaning | | ----------------------- | -------------- | ------------------------------------------------------------------------------------------------- | | `missing_access_token` | `401` | The route requires an authenticated access token, but none was available to the route guard. | | `missing_bearer_token` | `401` | The route expects an `Authorization: Bearer ...` header. | | `invalid_access_token` | `401` | The access JWT is malformed, expired, signed by an unknown key, or otherwise failed verification. | | `access_token_expired` | `401` | The access token is expired. Refresh and retry. | | `session_missing` | `401` or `404` | The server-side session was revoked, expired, or missing. Reauthenticate. | | `access_jti_mismatch` | `401` | The access token is no longer the current token for the session. Refresh or reauthenticate. | | `invalid_refresh_token` | `401` | The refresh token is invalid, expired, or already consumed. Reauthenticate. | | `refresh_expired` | SDK-side | The SDK knows the stored refresh token is past its expiry. Reauthenticate. | | `no_auth_session` | SDK-side | Auth was enabled, but no session is loaded. Sign in first. | | `user_only` | `403` | The route requires user auth. | | `admin_only` | `403` | The route requires admin auth. | | `insufficient_role` | `403` | The authenticated role cannot access the route. | # Collateral Source: https://docs.phoenix.trade/sdk/collateral Build Phoenix USDC deposit, withdrawal, and collateral verification flows with the TypeScript and Rust SDKs. Phoenix perps are margined in USDC. The SDK deposit and withdrawal helpers build Solana instructions; your app still signs and sends the transaction with the trader authority wallet. Deposit and withdrawal amounts are expressed in native USDC units when using the TypeScript SDK: `1 USDC = 1_000_000n`. The Rust `PhoenixTxBuilder` convenience methods accept decimal USDC amounts as `f64`. Deposit and withdrawal flows target a Phoenix trader account. Register the trader account first if it does not already exist. Withdrawals are subject to Phoenix account health checks and the protocol withdraw queue; see [Collateral](/phoenix/collateral-and-accounts/collateral) for more detail. ## TypeScript Use `createPhoenixClient(...)` and the `client.ixs` helpers when you want the SDK to resolve exchange accounts, Ember accounts, trader PDAs, and token accounts for you. `buildDepositIxs(...)` returns three instructions: 1. Create the Phoenix token ATA if needed. 2. Convert wallet USDC to Phoenix canonical tokens through Ember. 3. Deposit the Phoenix tokens into the Phoenix trader account. `buildWithdrawIxs(...)` returns five instructions: 1. Create the Phoenix token ATA if needed. 2. Approve Ember to spend Phoenix canonical tokens. 3. Create the USDC ATA if needed. 4. Withdraw Phoenix tokens from the Phoenix trader account. 5. Convert Phoenix tokens back to wallet USDC through Ember. ```ts TypeScript theme={null} import { createPhoenixClient } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", exchangeMetadata: { stream: false }, }); const authority = "AUTHORITY_PUBKEY"; const traderPdaIndex = 0; const traderSubaccountIndex = 0; const usdc = (wholeUsdc: bigint) => wholeUsdc * 1_000_000n; const deposit = await client.ixs.buildDepositIxs({ authority, traderPdaIndex, traderSubaccountIndex, amount: usdc(100n), }); const withdraw = await client.ixs.buildWithdrawIxs({ authority, traderPdaIndex, traderSubaccountIndex, amount: usdc(25n), }); console.log(deposit.instructions); console.log(withdraw.instructions); // Send each instruction array in a transaction signed by `authority`. // Wallet and transaction submission code depends on your Solana client stack. ``` You can also build the protocol instructions individually with `buildEmberDeposit`, `buildDepositFunds`, `buildWithdrawFunds`, and `buildEmberWithdraw`, but most integrations should prefer the full flow helpers above. ## Rust Use `PhoenixHttpClient` to fetch exchange metadata, then pass that metadata into `PhoenixTxBuilder`. The builder returns instruction arrays that should be sent in a single transaction signed by the trader authority. ```rust Rust theme={null} use phoenix_rise::{PhoenixHttpClient, PhoenixMetadata, PhoenixTxBuilder, TraderKey}; use solana_commitment_config::CommitmentConfig; use solana_keypair::read_keypair_file; use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_signer::Signer; use solana_transaction::Transaction; const RPC_ENDPOINT: &str = "https://api.mainnet-beta.solana.com"; let keypair = read_keypair_file("PATH_TO_KEYPAIR.json")?; let trader_key = TraderKey::new(keypair.pubkey()); let http = PhoenixHttpClient::new_from_env()?; let exchange = http.get_exchange().await?.into(); let metadata = PhoenixMetadata::new(exchange); let builder = PhoenixTxBuilder::new(&metadata); let rpc = RpcClient::new_with_commitment( RPC_ENDPOINT.to_string(), CommitmentConfig::confirmed(), ); let deposit_ixs = builder.build_deposit_funds( trader_key.authority(), trader_key.pda(), 100.0, )?; let deposit_blockhash = rpc.get_latest_blockhash().await?; let deposit_tx = Transaction::new_signed_with_payer( &deposit_ixs, Some(&trader_key.authority()), &[&keypair], deposit_blockhash, ); let deposit_signature = rpc.send_and_confirm_transaction(&deposit_tx).await?; let withdraw_ixs = builder.build_withdraw_funds( trader_key.authority(), trader_key.pda(), 25.0, )?; let withdraw_blockhash = rpc.get_latest_blockhash().await?; let withdraw_tx = Transaction::new_signed_with_payer( &withdraw_ixs, Some(&trader_key.authority()), &[&keypair], withdraw_blockhash, ); let withdraw_signature = rpc.send_and_confirm_transaction(&withdraw_tx).await?; println!("deposit: {deposit_signature}"); println!("withdraw: {withdraw_signature}"); ``` For a complete runnable Rust example, see [`rust/sdk/examples/deposit_funds.rs`](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/deposit_funds.rs). ## Monitoring After a transaction confirms, fetch trader state or collateral history to verify the account: ```ts TypeScript theme={null} const trader = await client.api .traders() .getTraderStateSnapshot(authority, { traderPdaIndex }); const subaccount = trader.snapshot.subaccounts.find( (subaccount) => subaccount.subaccountIndex === traderSubaccountIndex ); console.log("collateral:", subaccount?.collateral ?? "0"); const history = await client.api .collateral() .getTraderCollateralHistory(authority, { pdaIndex: traderPdaIndex, limit: 10, }); ``` ```rust Rust theme={null} use phoenix_rise::CollateralHistoryQueryParams; let traders = http.traders().get_trader(&trader_key.authority()).await?; if let Some(trader) = traders .iter() .find(|trader| trader.trader_subaccount_index == trader_key.subaccount_index) { println!("collateral: {}", trader.collateral_balance.ui); } let history = http .collateral() .get_trader_collateral_history( &trader_key, CollateralHistoryQueryParams::new(10), ) .await?; ``` # LiteSVM Testing Source: https://docs.phoenix.trade/sdk/litesvm-testing Use phoenix-rise-litesvm-test fixtures to test Phoenix program integrations locally. `phoenix-rise-litesvm-test` provides deterministic LiteSVM fixtures for Phoenix program integration tests. It loads Phoenix Eternal and Ember, optionally Hawkeye and Flight, creates fake USDC collateral, initializes markets, seeds actors, and exposes helpers for replaying setup and action transactions. References: * [phoenix-rise-litesvm-test README](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/litesvm-test/README.md) * [LiteSVM fixture crate](https://github.com/Ellipsis-Labs/rise-public/tree/master/rust/litesvm-test) * [SDK localnet fixture tests](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/tests/sdk_localnet_fixture_tests.rs) * [Example program tests](https://github.com/Ellipsis-Labs/rise-public/tree/master/programs/example-program/tests) * [TypeScript LiteSVM harness](https://github.com/Ellipsis-Labs/rise-public/tree/master/ts/tests/test-harness) * [TypeScript SDK localnet VM tests](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/tests/sdk-localnet-vm.test.ts) * [TypeScript SDK localnet flow tests](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/tests/sdk-localnet-flows.test.ts) ## Setup The fixture helpers look for local protocol SBF artifacts. In CI you can use the fixture package directly; locally, set the artifact paths or point the helpers at a Phoenix repo checkout. ```bash theme={null} export PHOENIX_REPO_ROOT=/path/to/phoenix export RISE_SDK_LOCALNET_REQUIRE_PROGRAMS=1 ``` For the example program tests, also build or point to the example program SBF: ```bash theme={null} export RISE_EXAMPLE_PROGRAM_SO=/path/to/rise_example_program.so ``` ## Context Build a `SdkLocalnetContext`, load any extra program under test, execute fixture setup, then send instructions through the context. ```rust Rust theme={null} use phoenix_rise_litesvm_test::{ SdkLocalnetContext, SdkLocalnetProgram, default_sdk_localnet_fixture, find_sdk_localnet_program_paths, }; use solana_pubkey::Pubkey; let fixture = default_sdk_localnet_fixture()?; let program_paths = find_sdk_localnet_program_paths() .expect("Phoenix protocol SBF artifacts are required"); let program_id = Pubkey::new_unique(); let mut context = SdkLocalnetContext::new_with_programs( fixture, program_paths, [SdkLocalnetProgram::new(program_id, "target/deploy/my_program.so")], ); context.execute_setup(); context.svm.warp_to_slot(200); ``` Source: [litesvm-test/src/context.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/litesvm-test/src/context.rs). ## Prime Markets Most order tests need current oracle and spline prices plus maker liquidity. The example program primes those from fixture transactions before sending taker instructions. ```rust Rust theme={null} pub fn prime_market(context: &mut SdkLocalnetContext) { context.send_fixture_transaction("oracleSetPrices"); context.send_fixture_transaction("splineUpdatePrices"); context.send_fixture_transaction("orderbookPlaceLevels"); } ``` Source: [example-program/tests/common/mod.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/common/mod.rs). ## Test A Flow Write tests against the behavior you expect from your program, not against Phoenix internals. This example places a market order, places a limit order, extracts the order id from return data/log helpers, and cancels it. ```rust Rust theme={null} #[test] fn market_limit_and_cancel_flow_executes() { let Some((mut context, program_id)) = setup_context() else { return; }; prime_market(&mut context); let actor = context.actor("taker0"); let market = context.market("BTC"); let market_logs = send_and_print_logs( &mut context, with_compute_budget(place_market_ix( &context, &actor, &market, program_id, Side::Bid, BTC_BASE_LOTS, quote_lots_for_price(&market, 100_500.0, 6, BTC_BASE_LOTS), 11, OrderFlags::None, )), &actor.seed, "place-market-order", ); assert_logs_contain(&market_logs, "Phoenix place market order"); } ``` Source: [example-program/tests/market\_orders.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/market_orders.rs). ## TypeScript LiteSVM The TypeScript SDK tests use the `litesvm` npm package with the same generated localnet fixture. The published package exposes the fixture at `@ellipsis-labs/rise/test-fixtures/default-localnet.json`; the public repo's test harness shows how to load local SBF artifacts, replay setup transactions, attach fixture signers, and send SDK-built instructions into LiteSVM. ```ts TypeScript theme={null} import { LiteSVM } from "litesvm"; import defaultFixture from "@ellipsis-labs/rise/test-fixtures/default-localnet.json"; const vm = new LiteSVM(); // Load local Phoenix/Ember/Hawkeye SBF artifacts, fund fixture signers, // then replay defaultFixture.setupTransactions before sending test actions. ``` Source: [ts/tests/test-harness/localnet.ts](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/tests/test-harness/localnet.ts). The public harness wraps those steps in `createSdkLocalnetContext` and exposes helpers for fixture-backed market params, account contexts, signing, and transaction submission. ```ts TypeScript theme={null} import { buildMarketOrderPacketFromMarketParams, buildPlaceMarketOrderIxResolved, Side, } from "@ellipsis-labs/rise"; import { buildSdkLocalnetMarketParams, buildSdkLocalnetPlaceOrderContext, createSdkLocalnetContext, } from "./test-harness/litesvm"; const context = await createSdkLocalnetContext(); const actor = context.getActor("taker0"); const orderPacket = buildMarketOrderPacketFromMarketParams( { side: Side.Bid, priceLimitUsd: "101000", baseUnits: "0.01", minBaseUnitsToFill: "0.01", }, buildSdkLocalnetMarketParams(context, "BTC") ); const ix = buildPlaceMarketOrderIxResolved({ ...buildSdkLocalnetPlaceOrderContext(context, { actorName: actor.name, symbol: "BTC", }), orderPacket, }); await context.sendInstructions([ix], { feePayerSeed: actor.seed, label: "sdk-built-place-market-order", }); ``` Source: [ts/tests/sdk-localnet-vm.test.ts](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/tests/sdk-localnet-vm.test.ts). The broader TypeScript flow tests cover the same localnet surface as the Rust fixture tests: deposits, withdrawals, market orders, limit orders, stop losses, conditionals, Hawkeye return data, and isolated subaccounts. ## Fixture Metadata When your off-chain test client needs the same market params as LiteSVM, build `PhoenixMetadata` from the fixture instead of calling mainnet APIs. ```rust Rust theme={null} use phoenix_rise_litesvm_test::{ default_sdk_localnet_fixture, phoenix_metadata_from_fixture, }; let fixture = default_sdk_localnet_fixture()?; let metadata = phoenix_metadata_from_fixture(&fixture)?; let builder = PhoenixTxBuilder::new(&metadata); ``` Use this pattern to test `PhoenixTxBuilder` flows against the local fixture and keep transaction construction deterministic. # Margin Source: https://docs.phoenix.trade/sdk/margin Compute Phoenix trader margin and liquidation estimates with the Rise SDK. Use this page when you need account health, margin requirements, transferable collateral, or liquidation estimates after you have live trader state. For account setup and trader-state subscriptions across subaccounts, see [Accounts](/sdk/accounts). ## Join Market Prices For margin and account health, subscribe to trader state and market stats for positions, orders, and collateral pricing markets—even when there is no position in the collateral asset. Preserve spot collateral balances and update both mark and index prices. See [Native SOL collateral](/sdk/native-sol-collateral) for the required upgrade and deposit flow. The margin examples below fetch current market prices and collateral configuration from the API. For a live view, recompute after trader-state, price, funding, or collateral-configuration updates; a previous result is a snapshot, not a live calculator. ## Local Margin Use trader-state `marginInputs()` and market params to compute local margin. For real-time monitoring, update market prices from WebSocket streams before recomputing. ```ts TypeScript theme={null} import { MarginMarketParamsStore } from "@ellipsis-labs/rise"; const paramsStore = new MarginMarketParamsStore({ client: client.api, }); // Includes spot collateral parameters and pricing-market index prices. const calculator = await paramsStore.getCalculator(); const marginInputs = resource.marginInputs(); if (marginInputs) { const margin = calculator.computeTraderMarginFromInputs(marginInputs); console.log(margin.subaccounts[0]?.margin); } ``` ```rust Rust theme={null} use phoenix_rise::api::PhoenixMetadata; let mut metadata = PhoenixMetadata::from_snapshot(http.get_exchange_snapshot().await?)?; for row in http.markets().get_latest_markets_stats().await?.markets { if metadata.get_market(&row.symbol).is_some() { metadata.apply_market_stats_v2(&row.into())?; } } if let Some(cross) = trader.primary_subaccount() { let portfolio = cross.to_trader_portfolio_with_metadata(&metadata)?; let margin = portfolio.compute_margin(&metadata)?; println!( "maintenance margin={}", margin.margin.maintenance_margin.as_inner() ); } ``` Reference: [Rust live margin example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/compute_trader_margin.rs). ## Cache-Backed Liquidation Estimates (Quote Collateral Only) For fast UI updates and risk monitoring, estimate liquidation prices from local trader state plus the exchange cache. This avoids API polling and uses the same SDK margin primitives described above. These custom examples support quote-only collateral. They reject spot collateral instead of omitting its value. For SOL-backed accounts, use the SDK margin flow above and [Hawkeye simulation](#hawkeye-simulation) for liquidation prices. The examples below binary-search the mark price where the subaccount risk tier crosses into `liquidatable` or worse. This keeps the estimate tied to SDK margin math, leverage tiers, funding, discounted uPnL, and limit-order margin. Treat it as a local estimate: stream freshness and search bounds matter. Use Hawkeye for the authoritative on-chain simulation. In TypeScript, the flow is `client.exchange.ready()`, `client.exchange.market(symbol)`, `client.marketData().market(symbol)`, `resource.marginInputs()`, then `createMarginCalculator(...)`. In Rust, the flow is `PhoenixMetadata::apply_market_stats(&stats)`, `Trader::apply_update(&msg)`, `SubaccountState::to_trader_portfolio()`, then `TraderPortfolio::compute_margin(metadata.all_perp_asset_metadata())`. ```ts TypeScript theme={null} import { createMarginCalculator, priceUsdToTicksWithMarketParams, type ExchangeMarketSnapshot, type MarketParams, type SubaccountMarginInputs, } from "@ellipsis-labs/rise"; const bps = (value: number | undefined, fallback: number) => String(value ?? fallback); const paramsFromExchangeCache = ( market: ExchangeMarketSnapshot, markPriceUsd: number ): MarketParams => ({ symbol: market.symbol, assetId: market.assetId, markPriceTicks: priceUsdToTicksWithMarketParams(markPriceUsd, { tickSize: market.tickSize, baseLotsDecimals: market.baseLotsDecimals, }).toString(), tickSize: market.tickSize.toString(), baseLotDecimals: market.baseLotsDecimals, leverageTiers: market.leverageTiers.map((tier) => ({ upperBoundSize: tier.maxSizeBaseLots.toString(), maxLeverage: String(tier.maxLeverage), limitOrderRiskFactorBps: String(tier.limitOrderRiskFactor), })), riskFactors: { maintenanceMarginFactorBps: bps( market.riskFactors.maintenanceBps, market.riskFactors.maintenance ), backstopMarginFactorBps: bps( market.riskFactors.backstopBps, market.riskFactors.backstop ), highRiskMarginFactorBps: bps( market.riskFactors.highRiskBps, market.riskFactors.highRisk ), }, cancelOrderRiskFactorBps: bps( market.riskFactors.cancelOrderBps, market.riskFactors.cancelOrder ), upnlRiskFactor: bps(market.riskFactors.upnlBps, market.riskFactors.upnl), upnlRiskFactorForWithdrawals: bps( market.riskFactors.upnlForWithdrawalsBps, market.riskFactors.upnlForWithdrawals ), isolatedOnly: market.isolatedOnly, }); const liquidationTiers = new Set([ "liquidatable", "backstopLiquidatable", "highRisk", ]); const estimateLiquidationPriceUsd = ( basePositionLots: bigint, currentPriceUsd: number, crossesLiquidation: (priceUsd: number) => boolean ) => { if (basePositionLots === 0n || currentPriceUsd <= 0) return null; if (crossesLiquidation(currentPriceUsd)) return currentPriceUsd; if (basePositionLots > 0n) { let low = Math.max(currentPriceUsd / 1_000_000, Number.EPSILON); let high = currentPriceUsd; if (!crossesLiquidation(low)) return null; for (let i = 0; i < 48; i++) { const mid = (low + high) / 2; if (crossesLiquidation(mid)) low = mid; else high = mid; } return high; } let low = currentPriceUsd; let high = currentPriceUsd * 2; for (let i = 0; i < 20 && !crossesLiquidation(high); i++) { low = high; high *= 2; } if (!crossesLiquidation(high)) return null; for (let i = 0; i < 48; i++) { const mid = (low + high) / 2; if (crossesLiquidation(mid)) high = mid; else low = mid; } return high; }; await client.exchange.ready(); const marketData = client.marketData(); const releaseMarketData = marketData.retain(); await marketData.ready(); const marginInputs = resource.marginInputs(); const subaccount = marginInputs?.subaccounts.find( (item) => item.subaccountIndex === 0 ); if (!subaccount) throw new Error("subaccount margin inputs are not ready"); if ( (subaccount.spotCollaterals ?? []).some((spot) => BigInt(spot.balance) !== 0n) || BigInt(subaccount.nativeSolCollateralLamports ?? "0") !== 0n ) { throw new Error("Use Hawkeye for SOL/spot-backed liquidation estimates"); } const paramsBySymbol: Record = {}; for (const marketInput of subaccount.markets) { const market = client.exchange.market(marketInput.symbol); const markPrice = marketData.market(marketInput.symbol)?.markPrice; if (!market || markPrice === null || markPrice === undefined) { throw new Error(`missing cached market data for ${marketInput.symbol}`); } paramsBySymbol[marketInput.symbol] = paramsFromExchangeCache(market, markPrice); } const symbol = "SOL"; const target = subaccount.markets.find((market) => market.symbol === symbol); const basePositionLots = BigInt(target?.position?.basePositionLots ?? "0"); const currentPrice = marketData.market(symbol)?.markPrice; if (currentPrice === null || currentPrice === undefined) { throw new Error(`missing mark price for ${symbol}`); } const crossesLiquidation = (priceUsd: number) => { const market = client.exchange.market(symbol); if (!market) throw new Error(`unknown market ${symbol}`); const calculator = createMarginCalculator( Object.values({ ...paramsBySymbol, [symbol]: paramsFromExchangeCache(market, priceUsd), }) ); const result = calculator.computeSubaccountMarginFromInputs( subaccount as SubaccountMarginInputs ); return liquidationTiers.has(result.margin.riskTier); }; const estimatedLiquidationPriceUsd = estimateLiquidationPriceUsd( basePositionLots, currentPrice, crossesLiquidation ); console.log(estimatedLiquidationPriceUsd); releaseMarketData(); ``` ```rust Rust theme={null} use phoenix_rise::api::PhoenixMetadata; use phoenix_rise::math::{RiskTier, TraderPortfolio}; fn estimate_liquidation_price_usd( portfolio: &TraderPortfolio, metadata: &PhoenixMetadata, symbol: &str, current_price: f64, ) -> Result, String> { if current_price <= 0.0 { return Ok(None); } let symbol = symbol.to_ascii_uppercase(); let Some(position) = portfolio.positions.get(&symbol) else { return Ok(None); }; if position.is_neutral() { return Ok(None); } let calculator = *metadata .get_market_calculator(&symbol) .ok_or_else(|| format!("missing market calculator for {symbol}"))?; let mut provider = metadata.all_perp_asset_metadata().clone(); let mut crosses_liquidation = |price: f64| -> Result { let mark_price = calculator .price_to_ticks(price) .map_err(|err| format!("{err:?}"))?; provider .get_mut(&symbol) .ok_or_else(|| format!("missing perp metadata for {symbol}"))? .set_mark_price(mark_price); let margin = portfolio .compute_margin(&provider) .map_err(|err| err.to_string())?; let tier = margin.risk_tier().map_err(|err| err.to_string())?; Ok(tier >= RiskTier::Liquidatable) }; if crosses_liquidation(current_price)? { return Ok(Some(current_price)); } if position.is_long() { let mut low = (current_price / 1_000_000.0).max(0.000001); let mut high = current_price; if !crosses_liquidation(low)? { return Ok(None); } for _ in 0..48 { let mid = (low + high) / 2.0; if crosses_liquidation(mid)? { low = mid; } else { high = mid; } } return Ok(Some(high)); } let mut low = current_price; let mut high = current_price * 2.0; for _ in 0..20 { if crosses_liquidation(high)? { break; } low = high; high *= 2.0; } if !crosses_liquidation(high)? { return Ok(None); } for _ in 0..48 { let mid = (low + high) / 2.0; if crosses_liquidation(mid)? { high = mid; } else { low = mid; } } Ok(Some(high)) } let cross = trader .primary_subaccount() .ok_or_else(|| "trader state is not ready".to_string())?; if cross.spot_collaterals.values().any(|spot| spot.balance != 0) { return Err("Use Hawkeye for SOL/spot-backed liquidation estimates".into()); } let portfolio = cross.to_trader_portfolio()?; let current_price = metadata .get_perp_asset_metadata("SOL") .and_then(|market| { metadata .get_market_calculator("SOL") .map(|calculator| calculator.ticks_to_price(market.mark_price)) }) .ok_or_else(|| "SOL market stats are not initialized".to_string())?; let estimated_liquidation_price_usd = estimate_liquidation_price_usd(&portfolio, &metadata, "SOL", current_price)?; println!("{estimated_liquidation_price_usd:?}"); ``` This estimate depends on the same cache freshness as margin monitoring. In TypeScript, keep `client.exchange` and `client.marketData()` live. In Rust, keep applying `PhoenixMetadata::apply_market_stats(&stats)` and trader-state updates before recomputing. ## Hawkeye Simulation For an authoritative liquidation-price view, use Hawkeye simulation through the SDK. This runs a read-only simulation and decodes the program return data. ```ts TypeScript theme={null} import { symbol } from "@ellipsis-labs/rise"; // `authority` is the trader owner's validated Authority address. const margin = await client.rpc.hawkeye.viewMargin({ authority, traderPdaIndex: 0, traderSubaccountIndex: 0, }); const liquidation = await client.rpc.hawkeye.viewLiquidationPrice({ authority, traderPdaIndex: 0, traderSubaccountIndex: 0, symbol: symbol("SOL"), }); if (margin.err || !margin.returnData) throw new Error("Margin simulation failed"); if (liquidation.err || !liquidation.returnData) throw new Error("Liquidation simulation failed"); console.log(margin.returnData.decoded); console.log(liquidation.returnData?.decoded.liquidationPriceTicks); ``` ```rust Rust theme={null} use phoenix_rise::core::{PhoenixHawkeyeClient, TraderKey}; let trader_key = TraderKey::new(authority); let hawkeye = PhoenixHawkeyeClient::new( "https://api.mainnet-beta.solana.com", &metadata, ); let margin = hawkeye .view_margin_for_trader(trader_key.pda()) .await?; let liquidation = hawkeye .view_liquidation_price_for_trader(trader_key.pda(), sol_asset_id) .await?; println!("margin={:?}", margin.value); println!( "liquidation price ticks={}", liquidation.value.liquidation_price_ticks ); ``` ## Isolated Order Estimates Server-built isolated order routes can also return the post-trade isolated liquidation estimate. ```ts TypeScript theme={null} const result = await client.api.orders().placeIsolatedMarketOrderEnhanced({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", side: "buy", numBaseLots: 67, transferAmount: 100_000_000, allowCrossAndIsolatedForAsset: true, }); console.log(result.estimatedLiquidationPriceUsd); ``` ```rust Rust theme={null} use phoenix_rise::ix::types::{IsolatedCollateralFlow, Side}; let (ixs, estimated_liq) = http .orders() .build_isolated_market_order_tx_enhanced( &authority, "SOL", Side::Bid, 67, Some(IsolatedCollateralFlow::TransferFromCrossMargin { collateral: 100_000_000 }), true, None, ) .await?; println!("estimated liquidation price: {:?}", estimated_liq); ``` # Exchange Data Source: https://docs.phoenix.trade/sdk/markets Use the Rise SDK exchange cache, market metadata, market stats, and orderbook streams. Use this page when your integration needs market params, public market metadata, live mark prices, L2 orderbooks, or commodity/equity calendar state. The exchange cache should be the default source for market params in both SDKs. It resolves the PerpAssetMap, global exchange accounts, market accounts, spline accounts, fee params, risk params, and public market metadata such as logos. Use REST to bootstrap, then let the SDK keep a local cache fresh instead of repeatedly polling the same params. ## Exchange Cache In TypeScript, configure `createPhoenixClient(...)` with exchange metadata streaming, wait for `client.exchange.ready()`, then read markets from the cache. ```ts TypeScript theme={null} import { createPhoenixClient } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", ws: { connectMode: "eager" }, exchangeMetadata: { stream: true }, }); const snapshot = await client.exchange.ready(); const sol = client.exchange.market("SOL"); const solMetadata = client.exchange.marketMetadata("SOL"); console.log(snapshot.markets.length); console.log(sol?.assetId, sol?.tickSize, sol?.baseLotsDecimals); console.log(solMetadata?.logoUri); const unsubscribe = client.exchange.onEvent((event) => { if (event.type === "marketUpdated") { console.log(event.symbol, event.change); } }); ``` ```rust Rust theme={null} use phoenix_rise::{PhoenixHttpClient, PhoenixMetadata}; let http = PhoenixHttpClient::new_from_env()?; let exchange = http.get_exchange().await?.into(); let mut metadata = PhoenixMetadata::new(exchange); let sol = metadata .get_market("SOL") .expect("SOL market should exist in exchange metadata"); println!("asset id: {}", sol.asset_id); println!("market: {}", sol.market_pubkey); println!("spline: {}", sol.spline_pubkey); ``` The TypeScript order-packet builders and `client.ixs` instruction builders read from `client.exchange`. The Rust `PhoenixTxBuilder` reads from `PhoenixMetadata`. Keep those caches warm before building orders so price, tick, base-lot, account, and risk-param conversions use the same market view as the rest of your app. References: * [TypeScript unified client example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/phoenix-client-example.ts) * [TypeScript limit-order ix example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/03-build-limit-order-ix.ts) * [Rust HTTP client example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/http_client.rs) * [Rust limit-order example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/send_limit_order.rs) ## Market Stats Market stats carry live mark price, mid price, oracle price, funding, volume, and open interest. Subscribe for current values; do not poll market stats in a tight loop. ```ts TypeScript theme={null} const controller = new AbortController(); for await (const update of client.streams!.marketStats("SOL", controller.signal)) { console.log( update.symbol, update.stats.markPrice, update.stats.currentFundingRate ); } ``` ```rust Rust theme={null} use phoenix_rise::api::{PhoenixMetadata, PhoenixWSClient}; let ws = PhoenixWSClient::new_from_env()?; let (mut rx, _handle) = ws.subscribe_to_market("SOL".to_string())?; while let Some(stats) = rx.recv().await { metadata.apply_market_stats(&stats)?; println!("{} mark=${}", stats.symbol, stats.mark_price); } ``` In Rust, `metadata.apply_market_stats(&stats)` updates the cached mark price inputs used by margin calculations. The TypeScript stream adapter converts the raw WebSocket payload into camelCase. `timestamp` is a `bigint` in TypeScript and is shown as a string below for display: ```json theme={null} { "symbol": "SOL", "stats": { "timestamp": "1719868800000", "openInterest": 1000.0, "markPrice": 150.1, "oraclePrice": 150.0, "prevDayMarkPrice": 148.7, "dayVolumeUsd": 2500000.0, "dayVolumeBase": 16655.1, "currentFundingRate": 0.0000125, "eightHourFundingRate": 0.0001, "annualizedFundingRate": 0.1095 } } ``` For raw WebSocket payloads, see [WebSocket market stats](/api/websocket#market-stats). For historical stats, use the REST route [`GET /v1/market/{symbol}/stats`](/api/get-market-stats-history). References: * [TypeScript WebSocket example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/phoenix-ws-example.ts) * [Rust market-stats example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/subscribe_market_stats.rs) ## L2 Orderbook Use the L2 stream when you need current book levels. Keep one subscription per symbol and fan out internally to your UI or strategy modules. ```ts TypeScript theme={null} const controller = new AbortController(); for await (const update of client.streams!.l2Book("SOL", controller.signal)) { console.log(update.symbol, update.bids[0], update.asks[0]); } ``` ```rust Rust theme={null} use phoenix_rise::api::PhoenixWSClient; use phoenix_rise::types::l2book::L2Book; let ws = PhoenixWSClient::new_from_env()?; let (mut rx, _handle) = ws.subscribe_to_orderbook_with_options("SOL".to_string(), false)?; let mut book = L2Book::new("SOL".to_string()); while let Some(update) = rx.recv().await { book.apply_update(&update); println!("bid={:?} ask={:?}", book.best_bid(), book.best_ask()); } ``` Reference: [Rust L2 book example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/subscribe_l2_book.rs). ## API Polling Polling is appropriate for startup snapshots, periodic reconciliation, or services that do not need live updates. It is not a substitute for WebSocket streams in a trading UI or market-making service. ```ts TypeScript theme={null} const snapshot = await client.api.exchange().getSnapshot(); const markets = await client.api.markets().getMarkets(); const sol = await client.api.markets().getMarket("SOL"); console.log(snapshot.markets.length, markets.length, sol.symbol); ``` ```rust Rust theme={null} let exchange = http.get_exchange().await?.into(); let metadata = PhoenixMetadata::new(exchange); let markets = http.markets().get_markets().await?; let sol = http.markets().get_market("SOL").await?; println!("{} {}", metadata.exchange().markets.len(), markets.len()); println!("{}", sol.symbol); ``` If you need to poll exchange params, poll on a slow interval and replace the local cache atomically. In TypeScript, prefer `exchangeMetadata: { stream: true }` when WebSocket access is available. In Rust, rebuild `PhoenixMetadata` from a fresh `get_exchange()` snapshot after reconnects or scheduled reconciliation, then continue applying market stats. ## Market Metadata And Calendars The exchange cache includes public market metadata used by apps, including labels and logo URIs where available. Treat these fields as display metadata; trading logic should use the market params and risk params from the cache. Commodity and equity markets can have session windows, after-hours bounds, reopen windows, and stale index behavior. The exchange cache exposes lightweight calendar metadata with the rest of the market snapshot so your UI can show the market's current calendar source and the next known transition without another API call. ```ts TypeScript theme={null} await client.exchange.ready(); const sol = client.exchange.market("SOL"); const publicMetadata = client.exchange.marketMetadata("SOL"); const cachedCalendar = sol?.metadata?.calendar ?? publicMetadata?.calendar; console.log(publicMetadata?.logoUri); console.log(cachedCalendar?.id); console.log(cachedCalendar?.calendarUri); console.log(cachedCalendar?.nextMarketTransitionUtc); ``` ```rust Rust theme={null} let sol = metadata .get_market("SOL") .expect("SOL market should exist in exchange metadata"); let cached_calendar = sol .metadata .as_ref() .and_then(|metadata| metadata.calendar.as_ref()); println!("{:?}", sol.metadata.as_ref().and_then(|m| m.logo_uri.as_ref())); println!("{:?}", cached_calendar.map(|calendar| &calendar.id)); println!("{:?}", cached_calendar.map(|calendar| &calendar.calendar_uri)); println!("{:?}", cached_calendar.and_then(|calendar| { calendar.next_market_transition_utc.as_ref() })); ``` Use the API clients when you need the full weekly schedule, date overrides, or current/next open state computed by the API. ```ts TypeScript theme={null} const calendar = await client.api.markets().getMarketCalendar("SOL"); const nextTransition = await client.api.markets().getNextMarketCalendarTransition("SOL"); const nextCommodityTransition = await client.api.markets().getNextCommodityMarketTransition(); const commodityCalendar = await client.api.markets().getCommodityMarketCalendar(); console.log(calendar.marketCalendarId, calendar.calendar?.weeklySchedule); console.log(nextTransition.currentState, nextTransition.utcNextTransition); console.log(nextCommodityTransition.market, nextCommodityTransition.nextMarketState); console.log(commodityCalendar.calendar.weeklySchedule); if (calendar.marketCalendarId) { const calendarRecord = await client.api.markets().getMarketCalendarById(calendar.marketCalendarId); console.log(calendarRecord.calendar?.weeklySchedule); } ``` ```rust Rust theme={null} let calendar = http.markets().get_market_calendar("SOL").await?; let next_transition = http .markets() .get_next_market_calendar_transition("SOL") .await?; let next_commodity_transition = http .markets() .get_next_commodity_market_transition() .await?; let commodity_calendar = http.markets().get_commodity_market_calendar().await?; println!("{}", calendar.market_calendar_id); println!("{:?}", calendar.calendar.as_ref().map(|c| &c.weekly_schedule)); println!("{:?}", next_transition.current_state); println!("{:?}", next_transition.utc_next_transition); println!("{:?}", next_commodity_transition.utc_next_transition); println!("{:?}", commodity_calendar.calendar.weekly_schedule); ``` TypeScript also exposes `getMarketCalendarById(id)` when you start from the calendar id stored in exchange metadata. Rust currently exposes the symbol and commodity calendar helpers. API reference: * [`GET /v1/market/{symbol}/market-calendar`](/api/get-market-calendar) * [`GET /v1/market-calendar/{market_calendar_id}`](/api/get-market-calendar-by-id) * [`GET /v1/market/{symbol}/next-market-calendar-transition`](/api/get-next-market-calendar-transition) * [`GET /v1/market/next-commodity-market-transition`](/api/get-next-commodity-market-transition) * [`GET /v1/market/commodity-calendar`](/api/get-commodity-market-calendar) Related Phoenix docs: * [RWA index price](/phoenix/real-world-assets/index-price) * [RWA mark price and bounds](/phoenix/real-world-assets/mark-price-and-bounds) * [Market specs](/phoenix/real-world-assets/market-specs) # Native SOL collateral Source: https://docs.phoenix.trade/sdk/native-sol-collateral Deposit SOL and include it in Rise TypeScript and Rust margin calculations. Native SOL stays in the trader account as lamports. No wrapping or USDC conversion is required. **Migration required — September 16, 2026 deployment** Native SOL collateral support is being deployed on September 16. Update your integration for this deployment: 1. Upgrade to the latest Rise SDK for TypeScript or Rust. 2. Use the deposit helper below to deposit SOL into a trader account. 3. Update your margin calculation using the example below. It includes the trader’s SOL balance, SOL’s index price, and the exchange’s collateral discount settings. If your app only counts USDC collateral, traders who deposit SOL will see less collateral and a worse account health score than they should. Updating the SDK is not enough if your app still leaves SOL out of its margin calculation. Depositing SOL adds collateral without changing the size, entry price, or direction of existing positions. The exchange counts a discounted portion of SOL’s value toward margin, so account health can change as SOL’s price changes. Use an existing trader and its owner's signing wallet, with API/RPC endpoints for the same deployment. ## Available instructions | Action | TypeScript | Rust | Details | | -------------------- | ------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Reserve capacity | `buildReallocTraderIx` | `create_realloc_trader_ix` | Reserves one position-map entry for the SOL collateral balance; payer funds any additional rent. Safe to call repeatedly: no-op when sufficient capacity already exists. | | Deposit | `buildNativeSolDepositFlow` | `create_deposit_native_sol_ixs` | Reserve capacity, transfer lamports, then sync collateral subject to caps. Sync is permissionless. | | Withdraw | `buildWithdrawNativeSolIx` | `create_withdraw_native_sol_ix` | Owner signs; system-owned destination. Accounted withdrawals are margin checked and throttled. | | Transfer traders | `buildTransferNativeSolIx` | `create_transfer_native_sol_ix` | Reserve destination capacity first; supply risk accounts and required authority. | | Sweep child → parent | `buildTransferNativeSolFromChildToParentIx` | `create_transfer_native_sol_from_child_to_parent_ix` | Flat isolated child; permissionless use depends on sweep preference. | | Swap SOL ↔ quote | `buildSwapNativeIx` | `create_swap_native_ix` | Supply venue instructions and minimum output: quote lots when selling SOL, lamports when buying. | | Liquidate SOL | `buildLiquidateNativeSolIx` | `create_liquidate_native_sol_ix` | Authorized liquidator/risk authority; venue instructions and maximum seized lamports. | ## Deposit SOL Send **ReallocTrader → System transfer → SyncNative** in one transaction. Native SOL consumes a position-map slot even though it is not a perp position. Reallocate first so additional rent is funded separately; `SyncNative` cannot resize the account. Call `ReallocTrader` once to extend the position map by one entry and reserve space for the SOL collateral balance. It is safe to include it in later deposits: when sufficient capacity already exists, it is a no-op. These examples derive PDA index `0`, subaccount `0` from the owner. Amounts are integer lamports: **0.01 SOL = 10,000,000 lamports**. Keep wallet SOL for rent and fees. ```ts TypeScript theme={null} import { buildNativeSolDepositFlow, type Authority, type PhoenixInstructionClient, } from "@ellipsis-labs/rise"; export async function nativeSolDeposit( client: PhoenixInstructionClient, owner: Authority, lamports = 10_000_000n, // 0.01 SOL ) { if (lamports <= 0n || lamports > 0xffff_ffff_ffff_ffffn) { throw new Error("Deposit must fit a positive u64"); } return buildNativeSolDepositFlow({ authority: owner, traderPdaIndex: 0, subaccountIndex: 0, lamports, }, client); } ``` ```rust Rust theme={null} use phoenix_rise::core::{PhoenixMetadata, TraderKey}; use phoenix_rise::ix::native_sol::{ create_deposit_native_sol_ixs, SyncNativeParams, }; use solana_instruction::Instruction; use solana_pubkey::Pubkey; pub fn native_sol_deposit( metadata: &PhoenixMetadata, owner: Pubkey, lamports: u64, // pass 10_000_000 for 0.01 SOL ) -> Result<(Pubkey, Vec), Box> { let trader = TraderKey::new(owner).pda(); let keys = metadata.keys(); let sync = SyncNativeParams::builder() .trader_account(trader) .global_trader_index(keys.global_trader_index.iter() .map(|key| key.parse()).collect::>()?) .active_trader_buffer(keys.active_trader_buffer.iter() .map(|key| key.parse()).collect::>()?) .build()?; let instructions = create_deposit_native_sol_ixs(owner, owner, lamports, sync)?; Ok((trader, instructions.into_iter().map(Into::into).collect())) } ``` The TypeScript helper takes a `PhoenixInstructionClient` configured with your deployment’s addresses and account fetcher (optionally its exchange cache). Both examples return unsigned instructions; the owner signs and sends them. Rust’s `create_deposit_native_sol_ixs` includes reallocation, transfer, and sync. TypeScript’s `buildNativeSolDepositFlow` also includes reallocation for ordinary wallet-paid deposits. Sponsored deposits require a separate wallet-paid `ReallocTrader` preparation transaction and `traderCapacityPrepared: true`; the sponsored transaction contains only transfer + sync. Have the wallet sign and submit with matching blockhash/preflight commitments. Confirm before reading margin. Caps can leave part of a successful deposit as uncredited excess, so always reread the accounted balance. ## Compute margin using API data | Input | SOL-specific requirement | | ------------------- | ------------------------------------------------------------------------------------------ | | Balance | Accounted SOL only; exclude rent and excess. Keep quote collateral separate. | | Collateral metadata | Asset index, decimals, pricing market, cap, and discount bounds from the collateral API. | | Prices | Include the SOL pricing market's **index price**, even without a SOL perp position. | | Existing state | Preserve positions, orders, funding, and risk parameters; refresh inputs when they change. | Effective collateral includes **discounted** SOL value; portfolio value includes **undiscounted** SOL value. SOL adds no perp margin requirement, but the BTC position does. SOL does not increase the quote-withdrawal payout cap: withdraw native SOL or explicitly swap it. Fetch market prices and collateral metadata from the API, then compute margin with the SDK. In TypeScript, use `getCalculator()` so collateral parameters are included. In Rust, use `to_trader_portfolio_with_metadata(&metadata)?` and pass `&metadata` to `compute_margin`; the quote-only `to_trader_portfolio()` path cannot value SOL. Supply current trader state from [Accounts](/sdk/accounts): TypeScript uses `resource.marginInputs()`; Rust uses the selected `SubaccountState` from the trader-state stream. Recompute after the confirmed deposit and order have appeared in that state. ```ts TypeScript theme={null} import { MarginMarketParamsStore, type PhoenixClient, type TraderMarginInputs, } from "@ellipsis-labs/rise"; export async function apiSolMargin( client: PhoenixClient, inputs: TraderMarginInputs, // resource.marginInputs(); includes spot balances ) { const params = new MarginMarketParamsStore({ client: client.api }); // Fetches market configuration, prices, and collateral metadata. const calculator = await params.getCalculator(); const margin = calculator.computeTraderMarginFromInputs(inputs); console.log("margin (quote lots)", margin.subaccounts); return margin; } ``` ```rust Rust theme={null} use phoenix_rise::api::{PhoenixHttpClient, PhoenixMetadata, SubaccountState}; use phoenix_rise::math::TraderPortfolioMargin; pub async fn api_sol_margin( http: &PhoenixHttpClient, subaccount: &SubaccountState, // current trader-state API snapshot ) -> Result> { // Exchange snapshot includes collateral parameters; stats supply prices. let mut metadata = PhoenixMetadata::from_snapshot(http.get_exchange_snapshot().await?)?; for row in http.markets().get_latest_markets_stats().await?.markets { if metadata.get_market(&row.symbol).is_some() { metadata.apply_market_stats_v2(&row.into())?; } } let portfolio = subaccount.to_trader_portfolio_with_metadata(&metadata)?; let margin = portfolio.compute_margin(&metadata)?; println!("margin (quote lots): {margin:#?}"); Ok(margin) } ``` These are local calculations using API prices, with index freshness assumed. Keep the SOL pricing market even without a SOL perp position, and never add SOL’s USD value to quote collateral yourself. Use Hawkeye below for an on-chain simulation. Margin and quote collateral use quote lots (`1 quote lot = 0.000001 USD`). Use the normal [order flow](/sdk/orders) to buy BTC: read the current bid/ask, compute the mid, and cap the buy price at `mid × (1 + slippagePercent / 100)` with a default of `1%`. SOL collateral does not change the order instruction. The deposit and order are separate transactions; a failed order leaves the deposit in place. ## View trader margin with Hawkeye Hawkeye simulates on-chain margin, including discounted SOL value. Call this after the deposit and again after the order. Margin and quote collateral use quote lots (`1 quote lot = 0.000001 USD`). ```ts TypeScript theme={null} import { decodeTrader, type PhoenixClient, type TraderAddress } from "@ellipsis-labs/rise"; export async function printSolMargin(client: PhoenixClient, traderAccount: TraderAddress) { const account = await client.rpc.accounts.fetchAccount(traderAccount); const trader = decodeTrader(account.data); const result = await client.rpc.hawkeye.viewMargin({ traderAccount }); if (result.err || !result.returnData) throw new Error("Hawkeye margin simulation failed"); console.log("accounted SOL lamports", trader.nativeSolCollateral.toString()); console.log("quote collateral", trader.state.quoteLotCollateral.toString()); console.log("margin (quote lots)", result.returnData.decoded); } ``` ```rust Rust theme={null} use phoenix_rise::accounts::trader::Trader; use phoenix_rise::core::PhoenixHawkeyeClient; use solana_pubkey::Pubkey; use solana_rpc_client::nonblocking::rpc_client::RpcClient; pub async fn print_sol_margin( rpc: &RpcClient, hawkeye: &PhoenixHawkeyeClient<'_>, trader: Pubkey, ) -> Result<(), Box> { let account = rpc.get_account(&trader).await?; let decoded = Trader::try_from_account_bytes(&account.data)?; println!("accounted SOL lamports: {}", decoded.native_sol_collateral()); println!("quote collateral: {}", decoded.header().trader_state.quote_lot_collateral.as_inner()); println!("margin (quote lots): {:#?}", hawkeye.view_margin_for_trader(trader).await?.value); Ok(()) } ``` # On-Chain Programs Source: https://docs.phoenix.trade/sdk/on-chain-programs Integrate Solana programs with Phoenix perpetuals through the Rise Rust SDK and CPI helpers. Use the Rise Rust CPI surface when your Solana program already has `AccountInfo` handles and needs to invoke Phoenix, Ember, Flight, or Hawkeye directly. The SDK does not fetch accounts or derive every PDA at CPI time. Your program validates and resolves accounts, then adapts them into typed CPI contexts such as `phoenix::PlaceMarketOrder`, `phoenix::PlaceStopLoss`, or `hawkeye::ViewMargin`. Reference implementations: * [rise-public programs/example-program](https://github.com/Ellipsis-Labs/rise-public/tree/master/programs/example-program) * [example-program/src/market.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/market.rs) * [example-program/src/place\_stop\_loss.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/place_stop_loss.rs) * [example-program/tests/common/mod.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/common/mod.rs) ## Example Program Map Use the example program as the source of truth for complete account lists. The docs below show the shape of each CPI, but the linked files show the full outer instruction, parameter struct, account metas, LiteSVM test setup, and log assertions. | Flow | Program code | Test coverage | | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Market order, limit order, cancel by id, matching-engine return data, and Hawkeye margin read | [market.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/market.rs), [place\_market\_order.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/place_market_order.rs), [place\_limit\_order.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/place_limit_order.rs), [cancel\_limit\_order.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/cancel_limit_order.rs) | [market\_orders.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/market_orders.rs) | | Ember wrap then Phoenix deposit | [deposit\_ember\_then\_phoenix.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/deposit_ember_then_phoenix.rs) | [deposit\_withdraw.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/deposit_withdraw.rs) | | Phoenix withdraw then Ember unwrap | [withdraw\_phoenix\_then\_ember.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/withdraw_phoenix_then_ember.rs) | [deposit\_withdraw.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/deposit_withdraw.rs) | | Stop-loss placement and cancellation | [place\_stop\_loss.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/place_stop_loss.rs), [cancel\_stop\_loss.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/cancel_stop_loss.rs) | [stop\_loss.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/stop_loss.rs) | | Register isolated subaccount, sync parent capabilities, transfer collateral, trade, close, and sweep | [register\_subaccount\_sync\_transfer\_and\_market\_order.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/register_subaccount_sync_transfer_and_market_order.rs), [close\_subaccount\_position\_and\_sweep.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/close_subaccount_position_and_sweep.rs) | [subaccounts.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/subaccounts.rs) | | Shared fixture setup, dynamic accounts, account metas, compute budget, and log helpers | [tests/common/mod.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/tests/common/mod.rs) | [programs/example-program/tests](https://github.com/Ellipsis-Labs/rise-public/tree/master/programs/example-program/tests) | ## Dependencies Most on-chain programs should depend on the `phoenix-rise` facade with only the `cpi` feature enabled. That profile exposes account byte decoders, instruction layouts, and Pinocchio CPI helpers without the HTTP, WebSocket, RPC, or transaction-builder graph. ```toml Cargo theme={null} [dependencies] phoenix-rise = { version = "0.3", default-features = false, features = ["cpi"] } pinocchio = "0.9.4" # Optional, but used by the public example program for instruction params, # program ids, and program-id validation helpers. borsh = { version = "1.6", features = ["derive"] } pinocchio-pubkey = "0.3" solana-pubkey = { version = "~3.0", default-features = false } ``` If you only want the instruction and CPI layer, depend on `phoenix-rise-ix` directly. ```toml Cargo theme={null} [dependencies] phoenix-rise-ix = { version = "0.3", default-features = false, features = ["cpi"] } pinocchio = "0.9.4" ``` The public example program uses the facade from the local workspace: ```toml Cargo theme={null} phoenix-rise = { path = "../../rust/sdk", default-features = false, features = [ "cpi", ] } ``` Source: [example-program/Cargo.toml](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/Cargo.toml). ## Imports Using the facade crate: ```rust Rust theme={null} use phoenix_rise::ix; use phoenix_rise::ix::types::{OrderFlags, SelfTradeBehavior, Side}; use pinocchio::account_info::AccountInfo; use pinocchio::program_error::ProgramError; use pinocchio::{ProgramResult, msg}; ``` Using the low-level instruction crate directly: ```rust Rust theme={null} use phoenix_rise_ix::cpi::{CpiScratch, hawkeye, phoenix}; use phoenix_rise_ix::types::{OrderFlags, SelfTradeBehavior, Side}; use pinocchio::account_info::AccountInfo; use pinocchio::program_error::ProgramError; ``` ## CPI Shape The typed CPI contexts are account-context structs. They borrow accounts from your outer instruction and encode the expected Phoenix account order for you. They do not own accounts and they do not resolve missing accounts from chain state. Phoenix order CPIs usually need: * Phoenix program id and log authority. * Global config and `PerpAssetMap`. * Trader signer or delegated authority, plus the trader account. * Market orderbook and spline collection. * Dynamic `global_trader_index` accounts. * Dynamic `active_trader_buffer` accounts. * Optional accounts for stop losses, conditionals, Hawkeye, Ember, or Flight. The example program keeps a fixed account prefix and passes the dynamic `global_trader_index` and `active_trader_buffer` accounts at the tail. The tail counts are carried in instruction data, so the loader can split the account slice without allocation. ```rust Rust theme={null} pub(crate) fn dynamic_tail<'a>( accounts: &'a [AccountInfo], fixed_account_count: usize, global_trader_index_count: usize, active_trader_buffer_count: usize, ) -> Result<(&'a [AccountInfo], &'a [AccountInfo]), ProgramError> { let gti_end = fixed_account_count .checked_add(global_trader_index_count) .ok_or(ProgramError::InvalidInstructionData)?; let expected = gti_end .checked_add(active_trader_buffer_count) .ok_or(ProgramError::InvalidInstructionData)?; require_exact_accounts(accounts, expected)?; Ok(( &accounts[fixed_account_count..gti_end], &accounts[gti_end..expected], )) } ``` Source: [example-program/src/common.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/common.rs). ## Market Context A useful integration pattern is to create one outer context that knows how to load and validate your instruction accounts, then add small helper methods that project those accounts into the SDK's typed CPI contexts. ```rust Rust theme={null} use phoenix_rise::ix; use pinocchio::account_info::AccountInfo; use pinocchio::program_error::ProgramError; use pinocchio::{ProgramResult, msg}; use crate::common::dynamic_tail; use crate::cpi::check_program_id; pub(crate) struct MarketContext<'a> { phoenix_program: &'a AccountInfo, hawkeye_program: &'a AccountInfo, log_authority: &'a AccountInfo, global_config: &'a AccountInfo, trader: &'a AccountInfo, trader_account: &'a AccountInfo, perp_asset_map: &'a AccountInfo, orderbook: &'a AccountInfo, spline_collection: &'a AccountInfo, global_trader_index: &'a [AccountInfo], active_trader_buffer: &'a [AccountInfo], } impl<'a> MarketContext<'a> { const FIXED_ACCOUNT_COUNT: usize = 9; pub(crate) fn load( accounts: &'a [AccountInfo], global_trader_index_count: usize, active_trader_buffer_count: usize, ) -> Result { let (global_trader_index, active_trader_buffer) = dynamic_tail( accounts, Self::FIXED_ACCOUNT_COUNT, global_trader_index_count, active_trader_buffer_count, )?; let context = Self { phoenix_program: &accounts[0], hawkeye_program: &accounts[1], log_authority: &accounts[2], global_config: &accounts[3], trader: &accounts[4], trader_account: &accounts[5], perp_asset_map: &accounts[6], orderbook: &accounts[7], spline_collection: &accounts[8], global_trader_index, active_trader_buffer, }; context.validate()?; Ok(context) } fn validate(&self) -> ProgramResult { check_program_id(self.phoenix_program, &ix::PHOENIX_PROGRAM_ID, "Phoenix")?; check_program_id(self.hawkeye_program, &ix::HAWKEYE_PROGRAM_ID, "Hawkeye")?; if !self.trader.is_signer() { msg!("trader must sign market action"); return Err(ProgramError::MissingRequiredSignature); } Ok(()) } fn place_market_order_accounts(&self) -> ix::cpi::phoenix::PlaceMarketOrder<'a> { ix::cpi::phoenix::PlaceMarketOrder { phoenix_program: self.phoenix_program, log_authority: self.log_authority, global_config: self.global_config, trader: self.trader, trader_account: self.trader_account, perp_asset_map: self.perp_asset_map, global_trader_index: self.global_trader_index, active_trader_buffer: self.active_trader_buffer, orderbook: self.orderbook, spline_collection: self.spline_collection, } } } ``` Source: [example-program/src/market.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/market.rs). ## Invoke Phoenix Once your outer context can build the typed CPI context, the invoke step is small: create `CpiScratch`, pass instruction-specific args, and let the SDK write the account metas and instruction data. ```rust Rust theme={null} use crate::cpi::MAX_CPI_ACCOUNTS; use crate::params::MarketOrderCpiParams; impl<'a> MarketContext<'a> { pub(crate) fn invoke_market_order( &self, params: &MarketOrderCpiParams, ) -> ProgramResult { let market_order = self.place_market_order_accounts(); let mut scratch = ix::cpi::CpiScratch::< { MAX_CPI_ACCOUNTS }, { ix::cpi::phoenix::PlaceMarketOrder::MAX_DATA_LEN }, >::new(self.phoenix_program); market_order.invoke( ix::cpi::phoenix::PlaceMarketOrderArgs { side: params.side, price_in_ticks: params.price_in_ticks, num_base_lots: params.num_base_lots, num_quote_lots: params.num_quote_lots, min_base_lots_to_fill: params.min_base_lots_to_fill, min_quote_lots_to_fill: params.min_quote_lots_to_fill, self_trade_behavior: params.self_trade_behavior, match_limit: params.match_limit, client_order_id: params.client_order_id, last_valid_slot: params.last_valid_slot, order_flags: params.order_flags, cancel_existing: params.cancel_existing, }, &mut scratch, ) } } ``` `CpiScratch` owns fixed-size stack buffers for account infos, account metas, and instruction data. For dynamic market accounts, size it to your integration's upper bound and check `ctx.account_count()` before invoking if the account count is user-controlled. If you already have reusable storage, use `CpiBuffers` and `invoke_with_buffers(...)` instead. ## Stop Losses And Conditionals Stop losses and conditional orders follow the same pattern, but the account context includes the funder, position authority, conditional or stop-loss account, and system program. Creating a stop-loss or conditional-order account can incur rent if the account does not already exist. ```rust Rust theme={null} let place_stop_loss = ix::cpi::phoenix::PlaceStopLoss { phoenix_program, log_authority, global_config, funder, trader_account, perp_asset_map, global_trader_index, active_trader_buffer, orderbook, spline_collection, position_authority, stop_loss_account, system_program, }; let mut scratch = ix::cpi::CpiScratch::< { MAX_CPI_ACCOUNTS }, { ix::cpi::phoenix::PlaceStopLoss::DATA_LEN }, >::new(phoenix_program); place_stop_loss.invoke( ix::cpi::phoenix::PlaceStopLossArgs { trigger_price, execution_price, trade_side, execution_direction, order_kind, }, &mut scratch, )?; ``` Source: [example-program/src/place\_stop\_loss.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/place_stop_loss.rs). ## Collateral CPIs Collateral flows often compose Ember and Phoenix CPIs in one outer instruction. The example program wraps fake USDC into Phoenix collateral through Ember, then deposits that collateral into the Phoenix trader account. ```rust Rust theme={null} let ember_deposit = ix::cpi::ember::EmberDeposit { ember_program, trader, ember_state, usdc_mint, canonical_mint, trader_usdc_account, trader_phoenix_account, ember_vault, token_program, }; let mut ember_scratch = ix::cpi::CpiScratch::< { ix::cpi::ember::EmberDeposit::ACCOUNT_COUNT }, { ix::cpi::ember::EmberDeposit::DATA_LEN }, >::new(ember_program); ember_deposit.invoke( ix::cpi::ember::EmberDepositArgs { amount: ember_amount }, &mut ember_scratch, )?; let phoenix_deposit = ix::cpi::phoenix::PhoenixDeposit { phoenix_program, log_authority, global_config, trader, trader_token_account: trader_phoenix_account, trader_account, global_vault, token_program, global_trader_index, active_trader_buffer, permission_account: None, }; let mut phoenix_scratch = ix::cpi::CpiScratch::< { MAX_CPI_ACCOUNTS }, { ix::cpi::phoenix::PhoenixDeposit::DATA_LEN }, >::new(phoenix_program); phoenix_deposit.invoke( ix::cpi::phoenix::PhoenixDepositArgs { amount: phoenix_amount }, &mut phoenix_scratch, )?; ``` Source: [example-program/src/deposit\_ember\_then\_phoenix.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/deposit_ember_then_phoenix.rs). Withdrawals reverse the sequence: Phoenix withdraw first, then Ember unwrap. Source: [example-program/src/withdraw\_phoenix\_then\_ember.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/withdraw_phoenix_then_ember.rs). ## Subaccount CPIs Subaccount flows are a good example of composing several typed contexts. The example registers the child trader account, syncs parent capabilities, transfers collateral to the child, then reuses `MarketContext` to place the child order. ```rust Rust theme={null} register.invoke( ix::cpi::phoenix::RegisterTraderArgs { max_positions, trader_pda_index, subaccount_index: child_subaccount_index, }, &mut register_scratch, )?; sync.invoke( ix::cpi::phoenix::SyncParentToChildArgs, &mut sync_scratch, )?; transfer.invoke( ix::cpi::phoenix::TransferCollateralArgs { amount }, &mut transfer_scratch, )?; let market_context = MarketContext::from_refs(MarketAccountRefs { phoenix_program, hawkeye_program, log_authority, global_config, trader: trader_authority, trader_account: child_trader_account, perp_asset_map, orderbook, spline_collection, global_trader_index, active_trader_buffer, })?; market_context.invoke_market_order_without_arenas(&order, "subaccount-open-order")?; ``` Source: [example-program/src/register\_subaccount\_sync\_transfer\_and\_market\_order.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/register_subaccount_sync_transfer_and_market_order.rs). ## Return Data Phoenix order CPIs can return matching-engine data. Decode return data instead of parsing logs. ```rust Rust theme={null} use phoenix_rise::ix::return_data::decode_matching_engine_cpi_response; use pinocchio::cpi::get_return_data; let Some(return_data) = get_return_data() else { return Ok(()); }; let response = decode_matching_engine_cpi_response(return_data.as_slice())?; if let Some(order_id) = response.order_id() { msg!(&format!( "order_id_price_in_ticks={} order_sequence_number={}", order_id.price_in_ticks, order_id.order_sequence_number, )); } ``` Source: [example-program/src/common.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/common.rs). ## Hawkeye Views Hawkeye is the read-only program for authoritative margin, liquidation-price, BBO, and funding views. Invoke it when local SDK math is not enough and you need the result the on-chain programs would use. Hawkeye writes versioned return data; decode those bytes instead of parsing logs. Hawkeye program id: `RiSeVw3ZjNfsaXPRb4mgaqYaEEt41pNNJoDvVh7pgQj`. Use it to validate the CPI program account on-chain and to assert that simulation return data came from Hawkeye off-chain. ```ts TypeScript theme={null} import { HAWKEYE_PROGRAM_ADDRESS } from "@ellipsis-labs/rise"; const result = await client.rpc.hawkeye.viewMargin({ authority: "AUTHORITY_PUBKEY", traderPdaIndex: 0, traderSubaccountIndex: 0, }); if (result.returnData?.programId !== HAWKEYE_PROGRAM_ADDRESS) { throw new Error("unexpected Hawkeye return-data program"); } ``` ```rust Rust theme={null} use phoenix_rise::ix; use pinocchio::account_info::AccountInfo; use pinocchio::ProgramResult; use crate::cpi::check_program_id; fn validate_hawkeye_program(hawkeye_program: &AccountInfo) -> ProgramResult { check_program_id(hawkeye_program, &ix::HAWKEYE_PROGRAM_ID, "Hawkeye") } ``` Hawkeye instructions are normal Solana instructions, so they can be invoked off-chain in a transaction simulation or on-chain through CPI. The current typed Pinocchio CPI helper in the Rust SDK is `ix::cpi::hawkeye::ViewMargin`; the other views are exposed through off-chain instruction builders and can be mirrored on-chain with the same account metas and discriminators if your program needs them. | View | Hawkeye ix | Return data | SDK entry points | | ----------------- | ------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Account margin | `view_margin` | `ViewMarginReturn` / `HawkeyeMarginReturn` | On-chain `ix::cpi::hawkeye::ViewMargin`; TypeScript `client.rpc.hawkeye.viewMargin(...)` or `buildHawkeyeViewMarginIx(...)`; Rust `PhoenixHawkeyeClient::view_margin(...)` or `create_hawkeye_view_margin_ix(...)` | | Asset margin | `view_margin_for_asset` | `ViewAssetReturn` / `HawkeyeAssetReturn` | TypeScript `client.rpc.hawkeye.viewMarginForAsset(...)` or `buildHawkeyeViewMarginForAssetIx(...)`; Rust `PhoenixHawkeyeClient::view_margin_for_asset(...)` or `create_hawkeye_view_margin_for_asset_ix(...)` | | Liquidation price | `view_liquidation_price` | `ViewLiquidationPriceReturn` / `HawkeyeLiquidationPriceReturn` | TypeScript `client.rpc.hawkeye.viewLiquidationPrice(...)` or `buildHawkeyeViewLiquidationPriceIx(...)`; Rust `PhoenixHawkeyeClient::view_liquidation_price(...)` or `create_hawkeye_view_liquidation_price_ix(...)` | | Best bid/offer | `view_bbo` | `ViewBboReturn` / `HawkeyeBboReturn` | TypeScript `client.rpc.hawkeye.viewBbo(...)` or `buildHawkeyeViewBboIx(...)`; Rust `PhoenixHawkeyeClient::view_bbo(...)` or `create_hawkeye_view_bbo_ix(...)` | | Funding | `view_funding` | `ViewFundingReturn` / `HawkeyeFundingReturn` | TypeScript `client.rpc.hawkeye.viewFunding(...)` or `buildHawkeyeViewFundingIx(...)`; Rust `PhoenixHawkeyeClient::view_funding(...)` or `create_hawkeye_view_funding_ix(...)` | Sources: [rust/ix/src/hawkeye.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/ix/src/hawkeye.rs), [ts/src/hawkeye.ts](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/hawkeye.ts), [rust/core/src/hawkeye\_client.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/core/src/hawkeye_client.rs), and [ts/src/rpc.ts](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/src/rpc.ts). ### On-chain CPI On-chain programs read Hawkeye return data the same way they read Phoenix return data: invoke the program, call `get_return_data()` immediately, assert the return-data program id is Hawkeye, then decode the expected struct. Return data is overwritten by the next CPI that sets it, so decode or copy it before another Phoenix or Hawkeye call. The example program invokes `hawkeye::ViewMargin` after market orders and decodes the return into `ViewMarginReturn`. ```rust Rust theme={null} let view_margin = ix::cpi::hawkeye::ViewMargin { hawkeye_program, phoenix_program, global_config, global_trader_index, active_trader_buffer, perp_asset_map, trader: trader_account, }; let mut scratch = ix::cpi::CpiScratch::< { MAX_CPI_ACCOUNTS }, { ix::cpi::hawkeye::ViewMargin::DATA_LEN }, >::new(hawkeye_program); let margin = view_margin.invoke_and_decode(&mut scratch)?; msg!(&format!( "collateral={} free={} maintenance={} liquidatable={}", margin.collateral_quote_lots, margin.free_collateral_quote_lots, margin.maintenance_margin_quote_lots, margin.is_liquidatable, )); ``` If you need the manual decode path, use the same guard the helper uses internally. ```rust Rust theme={null} use phoenix_rise::ix::hawkeye::{ViewMarginReturn, decode_hawkeye_return}; use pinocchio::cpi::get_return_data; let return_data = get_return_data().ok_or(ProgramError::InvalidAccountData)?; if return_data.program_id() != &ix::HAWKEYE_PROGRAM_ID.to_bytes() { return Err(ProgramError::InvalidAccountData); } let margin = decode_hawkeye_return::( return_data.as_slice(), "view_margin", ) .map_err(|_| ProgramError::InvalidAccountData)?; ``` Source: [example-program/src/market.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/programs/example-program/src/market.rs) and [rust/ix/src/cpi.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/ix/src/cpi.rs). ### Off-chain simulation For applications and services, prefer the SDK Hawkeye RPC helpers. They build a read-only simulation transaction, validate that return data came from the Hawkeye program id, and decode the bytes into the typed return shape. ```ts TypeScript theme={null} const margin = await client.rpc.hawkeye.viewMargin({ authority: "AUTHORITY_PUBKEY", traderPdaIndex: 0, traderSubaccountIndex: 0, }); const liquidation = await client.rpc.hawkeye.viewLiquidationPrice({ authority: "AUTHORITY_PUBKEY", traderPdaIndex: 0, traderSubaccountIndex: 0, symbol: "SOL", }); const bbo = await client.rpc.hawkeye.viewBbo({ symbol: "SOL" }); console.log(margin.returnData?.decoded.freeCollateralQuoteLots); console.log(liquidation.returnData?.decoded.liquidationPriceTicks); console.log(bbo.returnData?.decoded.markPriceTicks); ``` ```rust Rust theme={null} use phoenix_rise::{PhoenixHawkeyeClient, TraderKey}; let trader_key = TraderKey::new(authority); let hawkeye = PhoenixHawkeyeClient::new(rpc_url, &metadata); let margin = hawkeye .view_margin_for_trader(trader_key.pda()) .await?; let liquidation = hawkeye .view_liquidation_price_for_trader(trader_key.pda(), sol_asset_id) .await?; let bbo = hawkeye.view_bbo("SOL").await?; println!("free collateral={}", margin.value.free_collateral_quote_lots); println!( "liquidation price ticks={}", liquidation.value.liquidation_price_ticks ); println!("mark price ticks={}", bbo.value.mark_price_ticks); ``` If you already have accounts resolved, you can build a Hawkeye instruction directly and still let the SDK decode the return data. ```ts TypeScript theme={null} import { buildHawkeyeViewLiquidationPriceIx } from "@ellipsis-labs/rise"; const ix = buildHawkeyeViewLiquidationPriceIx({ phoenixProgramAddress, globalConfigurationAddress, globalTraderIndex, activeTraderBuffer, perpAssetMap, traderAccount, assetId: 0, }); const simulation = await client.rpc.hawkeye.simulateInstruction(ix); const decoded = simulation.returnData?.decoded ?? null; if (decoded?.kind === "view_liquidation_price") { console.log(decoded.liquidationPriceTicks); } ``` ```rust Rust theme={null} use phoenix_rise::ix::hawkeye::{ HawkeyeReturnData, HawkeyeTraderViewAccounts, create_hawkeye_view_liquidation_price_ix, }; let accounts = HawkeyeTraderViewAccounts { phoenix_program_id, global_config, global_trader_index, active_trader_buffer, perp_asset_map, trader: trader_account, }; let ix = create_hawkeye_view_liquidation_price_ix(accounts, sol_asset_id); let simulation = hawkeye.simulate_instruction(ix.into()).await?; if let HawkeyeReturnData::LiquidationPrice(value) = simulation.value { println!("{}", value.liquidation_price_ticks); } ``` Reference coverage: [ts/tests/sdk-localnet-flows.test.ts](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/tests/sdk-localnet-flows.test.ts) executes all five Hawkeye views in LiteSVM, and [rust/sdk/tests/sdk\_localnet\_fixture\_tests.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/tests/sdk_localnet_fixture_tests.rs) asserts Hawkeye return data is emitted by the expected program id. ## Supported CPI Contexts The typed CPI surface includes: * Trader lifecycle: `RegisterTrader`, `SetTraderCapabilitiesDelegated`, `UpdateTraderState`, `SyncParentToChild`. * Order flow: `PlaceMarketOrder`, `PlaceLimitOrder`, `PlaceMarketOrderDelegated`, `PlaceMultiLimitOrder`, `CancelAll`, `CancelUpTo`, `CancelOrdersById`, `CancelAllPlusConditional`. * Collateral and subaccounts: `PhoenixDeposit`, `PhoenixWithdraw`, `TransferCollateral`, `TransferCollateralChildToParent`. * Stop losses and conditionals: `CreateConditionalOrdersAccount`, `PlaceStopLoss`, `CancelStopLoss`, `PlacePositionConditionalOrder`, `PlaceAttachedConditionalOrder`, `PlaceLimitOrderWithConditionals`, `CancelConditionalOrder`. * Ember collateral movement: `ember::EmberDeposit`, `ember::EmberWithdraw`. * Hawkeye reads: `hawkeye::ViewMargin` / `hawkeye::HawkeyeViewMargin`. For full account lists and data lengths, inspect [rust/ix/src/cpi.rs](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/ix/src/cpi.rs). ## Testing Use [LiteSVM Testing](/sdk/litesvm-testing) for local integration tests. The example program tests cover deposits, withdrawals, market orders, limit orders, cancels, stop losses, Hawkeye reads, and isolated subaccount collateral flows. # Orders Source: https://docs.phoenix.trade/sdk/orders Build Phoenix order, cancellation, stop-loss, and conditional-order instructions with the Rise SDK. Use this page when a trader account already exists and you want to build Solana instructions for trading. Your app still signs and sends the transaction with the correct authority, position authority, delegated wallet, or fee payer. For account indexes, cross vs isolated accounts, and trader-state subscriptions across all subaccounts, see [Accounts](/sdk/accounts). For mark-price joins, account health, and liquidation estimates, see [Margin](/sdk/margin). ## Instruction Builders All order builders produce Phoenix instructions. They do not submit transactions by themselves. | Action | TypeScript builder | Rust builder | Phoenix instruction | | ------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------- | | Place limit order | `client.ixs.buildPlaceLimitOrder(...)` | `PhoenixTxBuilder::place_limit_order(...)` | `PlaceLimitOrder` | | Cancel by order id | `client.ixs.buildCancelOrdersById(...)` | `PhoenixTxBuilder::build_cancel_orders(...)` | `CancelOrdersById` | | Cancel all in a market | `client.ixs.buildCancelAll(...)` | `PhoenixTxBuilder::build_cancel_all_orders(...)` | `CancelAll` | | Place market order | `client.ixs.buildPlaceMarketOrder(...)` | `PhoenixTxBuilder::place_market_order(...)` | `PlaceMarketOrder` | | Place position stop loss | `client.ixs.buildPlaceStopLoss(...)` | `PhoenixTxBuilder::build_stop_loss_orders(...)` | `PlaceStopLoss` | | Place conditional order | `client.ixs.buildPlacePositionConditionalOrder(...)` | `PhoenixTxBuilder::place_position_bracket_order(...)` or raw `ix` builders | `PlacePositionConditionalOrder` | ## Limit Order ```ts TypeScript theme={null} import { Side } from "@ellipsis-labs/rise"; await client.exchange.ready(); const orderPacket = await client.orderPackets.buildLimitOrderPacket({ symbol: "SOL", side: Side.Bid, priceUsd: "150.50", baseUnits: "0.25", }); const ix = await client.ixs.buildPlaceLimitOrder({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", orderPacket, traderPdaIndex: 0, traderSubaccountIndex: 0, }); ``` ```rust Rust theme={null} use phoenix_rise::{LimitOrderTicket, PhoenixTxBuilder, Side, TraderKey}; let trader = TraderKey::new(authority); let builder = PhoenixTxBuilder::new(&metadata); let ticket = LimitOrderTicket::builder() .authority(trader.authority()) .trader_account(trader.pda()) .symbol("SOL") .side(Side::Bid) .price(150.50) .num_base_lots(50_000) .build()?; let ixs = builder.place_limit_order(ticket).await?; ``` References: * [TypeScript limit-order example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/03-build-limit-order-ix.ts) * [Rust limit-order example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/send_limit_order.rs) ## Cancel Limit Order ```ts TypeScript theme={null} const ix = await client.ixs.buildCancelOrdersById({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", orders: [ { price: 50_000n, orderSequenceNumber: "12345", }, ], traderPdaIndex: 0, traderSubaccountIndex: 0, }); ``` ```rust Rust theme={null} use phoenix_rise::ix::types::CancelId; let cancel_id = CancelId::new(50_000, 12_345); let ixs = builder.build_cancel_orders( trader.authority(), trader.pda(), "SOL", vec![cancel_id], )?; ``` Reference: [Rust cancel-order example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/cancel_order.rs). ## Cancel All ```ts TypeScript theme={null} const ix = await client.ixs.buildCancelAll({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", traderPdaIndex: 0, traderSubaccountIndex: 0, }); ``` ```rust Rust theme={null} let ixs = builder.build_cancel_all_orders( trader.authority(), trader.pda(), "SOL", )?; ``` ## Market Order ```ts TypeScript theme={null} import { Side } from "@ellipsis-labs/rise"; const orderPacket = await client.orderPackets.buildMarketOrderPacket({ symbol: "SOL", side: Side.Bid, baseUnits: "0.25", }); const ix = await client.ixs.buildPlaceMarketOrder({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", orderPacket, traderPdaIndex: 0, traderSubaccountIndex: 0, }); ``` ```rust Rust theme={null} use phoenix_rise::{MarketOrderTicket, Side}; let ticket = MarketOrderTicket::builder() .authority(trader.authority()) .trader_account(trader.pda()) .symbol("SOL") .side(Side::Bid) .num_base_lots(67) .build()?; let ixs = builder.place_market_order(ticket).await?; ``` Reference: [Rust market-order example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/send_market_order.rs). ## Stop Losses Position stop losses use a per-trader, per-asset stop-loss account. If the account does not already exist, creating it can add an account-init instruction and incur rent. ```ts TypeScript theme={null} import { Direction, Side, StopLossOrderKind } from "@ellipsis-labs/rise"; const ix = await client.ixs.buildPlaceStopLoss({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", triggerPrice: 140_000n, slippageBps: 100, tradeSide: Side.Ask, executionDirection: Direction.LessThan, orderKind: StopLossOrderKind.IOC, traderPdaIndex: 0, traderSubaccountIndex: 0, }); ``` ```rust Rust theme={null} use phoenix_rise::{BracketLeg, BracketLegOrders, Side}; let bracket = BracketLegOrders { stop_loss: Some(BracketLeg::new(140.0).with_slippage_bps(100)), take_profit: None, }; let ixs = builder.build_stop_loss_orders( trader.authority(), trader.pda(), "SOL", Side::Bid, &bracket, )?; ``` To cancel a position stop loss: ```ts TypeScript theme={null} import { Direction } from "@ellipsis-labs/rise"; const ix = await client.ixs.buildCancelStopLoss({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", executionDirection: Direction.LessThan, traderPdaIndex: 0, traderSubaccountIndex: 0, }); ``` ```rust Rust theme={null} use phoenix_rise::Direction; let ixs = builder.build_cancel_bracket_leg( trader.authority(), trader.pda(), "SOL", Direction::LessThan, )?; ``` Reference: [Rust cancel stop-loss example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/cancel_stop_loss.rs). ## Conditional Orders Conditional orders use a trader conditional-orders account. If the account does not already exist, your flow may need to create it first and pay rent. The newer bracket helpers can check and create the account when configured with an RPC client; raw builders require you to include `CreateConditionalOrdersAccount` yourself when needed. ```ts TypeScript theme={null} import { Direction, Side, StopLossOrderKind, ticks, } from "@ellipsis-labs/rise"; declare const conditionalOrdersAccountExists: boolean; const ixs = []; if (!conditionalOrdersAccountExists) { ixs.push( await client.ixs.buildCreateConditionalOrdersAccount({ authority: "AUTHORITY_PUBKEY", traderPdaIndex: 0, traderSubaccountIndex: 0, }) ); } ixs.push( await client.ixs.buildPlacePositionConditionalOrder({ authority: "AUTHORITY_PUBKEY", symbol: "SOL", lessTriggerOrder: { triggerDirection: Direction.LessThan, tradeSide: Side.Ask, orderKind: StopLossOrderKind.IOC, triggerPrice: ticks(140_000n), slippageBps: 100, }, sizePercent: 100, traderPdaIndex: 0, traderSubaccountIndex: 0, }) ); ``` ```rust Rust theme={null} use std::sync::Arc; use phoenix_rise::{ BracketLeg, BracketLegOrders, BracketLegTicket, Side, }; use solana_rpc_client::nonblocking::rpc_client::RpcClient; let rpc = Arc::new(RpcClient::new("https://api.mainnet-beta.solana.com".to_string())); let bracket = BracketLegOrders { stop_loss: Some(BracketLeg::new(140.0).with_slippage_bps(100)), take_profit: Some(BracketLeg::new(180.0).with_limit_order()), }; let ixs = builder .place_position_bracket_order( trader.authority(), trader.pda(), "SOL", Side::Bid, BracketLegTicket::new(rpc, bracket), ) .await?; ``` For bulk cancellation, read the conditional-orders account, enumerate active trigger legs, and build one cancel instruction per active leg. The public examples show the full account-read and batching flow: * [TypeScript cancel all conditional orders](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/05-cancel-all-conditional-orders.ts) * [Rust cancel all conditional orders](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/cancel_all_conditional_orders.rs) # Trader Onboarding Source: https://docs.phoenix.trade/sdk/register Activate Phoenix trader accounts with or without a referral code. Do not build new integrations around `/v1/invite/activate` or `/v1/referral/activate`. These legacy sponsored activation routes are deprecated; use `/v1/referral/activate-tx` for referral-code onboarding. Most users should onboard in the Phoenix app at [phoenix.trade](https://phoenix.trade). Use the SDK flows below when you are building onboarding into your own app. There are two SDK onboarding paths: 1. With a referral code: call `POST /v1/referral/activate-tx`. This path requires auth and the user-controlled trader authority signature. 2. Without a referral code: call `POST /v1/exchange/build-register-ixs`, build and sign the transaction locally, then submit it to `POST /v1/exchange/send-register-ixs`. This path does not validate the trader authority signature and can register off-curve authorities such as PDAs. Signer handling differs by path. For referral-code activation, the request must include an authenticated Phoenix session, the user authority must sign the transaction request, and the API adds the Phoenix referral onboarder signature before submitting it. For the no-referral builder flow, `traderAuthority` is only the pubkey used to derive the trader account; it is not required to be a signer. The transaction sent to `send-register-ixs` must be signed by the fee payer and any other signer accounts you add. In the no-referral builder flow, `pda_index` and `subaccount_index` must be `0`; `max_positions` is optional and defaults to `128`. Trader onboarding registers the default cross-margin account at `traderPdaIndex = 0` and `traderSubaccountIndex = 0`. Its `max_positions` value must be between `32` and `128` inclusive. Omit the field to use the default `128`. ## With a referral code Use `/v1/referral/activate-tx` when the user has a valid referral code. The TypeScript SDK builds the transaction, checks whether the default trader account needs to be registered, and submits the signed transaction request to the API. If your app pays for onboarding, pass `feePayer` and sign the transaction with both the payer and the trader authority. If the trader pays, omit `feePayer`. ```ts TypeScript theme={null} import { createPhoenixClient, type Authority } from "@ellipsis-labs/rise"; import { createKeyPairSignerFromBytes } from "@solana/signers"; import { createSolanaRpc, partiallySignTransaction, } from "@solana/kit"; declare const traderKeypairBytes: Uint8Array; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", exchangeMetadata: { stream: false }, }); const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com"); const traderSigner = await createKeyPairSignerFromBytes(traderKeypairBytes); const traderAuthority = traderSigner.address as Authority; const latestBlockhash = await rpc .getLatestBlockhash({ commitment: "finalized" }) .send(); const built = await client.api.invite().buildActivateReferralTxRequest({ referralCode: "REFERRAL_CODE", traderAuthority, traderPdaIndex: 0, traderSubaccountIndex: 0, recentBlockhash: latestBlockhash.value.blockhash, lastValidBlockHeight: latestBlockhash.value.lastValidBlockHeight, registerTraderMaxPositions: 128n, rpc, signTransaction: (transaction) => partiallySignTransaction([traderSigner.keyPair], transaction), }); const response = await client.api.invite().activateReferralTx(built.request); console.log(response.signature, response.trader_pda, response.status); ``` ```rust Rust theme={null} use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use phoenix_rise::{ ActivateReferralTxRequest, PhoenixHttpClient, PhoenixMetadata, TraderKey, fetch_referral_activation_trader_status, }; use phoenix_rise::phoenix_rise_ix::{ OnboardTraderDelegatedParams, RegisterTraderParams, create_onboard_trader_delegated_ix, create_register_trader_ix, }; use solana_commitment_config::CommitmentConfig; use solana_instruction::Instruction; use solana_keypair::read_keypair_file; use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_signer::Signer; use solana_transaction::Transaction; use solana_transaction::versioned::VersionedTransaction; let http = PhoenixHttpClient::new_public("https://perp-api.phoenix.trade")?; let rpc = RpcClient::new_with_commitment( "https://api.mainnet-beta.solana.com".to_string(), CommitmentConfig::confirmed(), ); let trader_keypair = read_keypair_file("/path/to/trader-keypair.json")?; let trader = TraderKey::new_with_idx(trader_keypair.pubkey(), 0, 0); let permission = http.invite().get_referral_activation_permission().await?; let exchange = PhoenixMetadata::new(http.get_exchange().await?.into()); let status = fetch_referral_activation_trader_status(&rpc, &trader.pda()).await?; let mut ixs = Vec::::new(); if status.should_include_register_trader() { ixs.push( create_register_trader_ix( RegisterTraderParams::builder() .payer(trader_keypair.pubkey()) .trader(trader.authority()) .trader_account(trader.pda()) .max_positions(128) .trader_pda_index(0) .subaccount_index(0) .build()?, )? .into(), ); } ixs.push( create_onboard_trader_delegated_ix( OnboardTraderDelegatedParams::builder() .authority(permission.trader_onboarder.parse()?) .permission_account(permission.permission_account.parse()?) .trader_account(trader.pda()) .global_trader_index( exchange.keys().global_trader_index .iter() .map(|key| key.parse()) .collect::, _>>()?, ) .active_trader_buffer( exchange.keys().active_trader_buffer .iter() .map(|key| key.parse()) .collect::, _>>()?, ) .build()?, )? .into(), ); let recent_blockhash = rpc.get_latest_blockhash().await?; let mut tx = Transaction::new_with_payer(&ixs, Some(&trader_keypair.pubkey())); tx.try_partial_sign(&[&trader_keypair], recent_blockhash)?; let transaction = BASE64_STANDARD.encode(bincode::serialize( &VersionedTransaction::from(tx), )?); let response = http.invite().activate_referral_tx(&ActivateReferralTxRequest { referral_code: "REFERRAL_CODE".to_string(), trader_authority: trader.authority().to_string(), trader_pda_index: Some(0), trader_subaccount_index: Some(0), recent_blockhash: recent_blockhash.to_string(), transaction, }).await?; println!("{response:?}"); ``` Full runnable examples: * [TypeScript referral activation](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/09-referral-activation-tx.ts) * [Rust referral activation](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/referral_activation_tx.rs) ## Without a referral code Use this path when a builder wants to register and onboard a trader without a referral code. The flow is: 1. Call `POST /v1/exchange/build-register-ixs` with the trader authority pubkey and the wallet that will pay transaction fees and account rent. 2. Build a Solana transaction locally with those returned instructions, your current blockhash, and your chosen fee payer. 3. Sign the transaction with the fee payer and any other signer accounts you add. 4. Send the base64-encoded wire transaction to `POST /v1/exchange/send-register-ixs`. 5. The API validates the transaction, signs with the Phoenix onboarder, simulates it, verifies the onboarder pays no lamports, sends it to Phoenix's RPC, and returns the transaction signature. The no-referral path does not validate that `traderAuthority` can sign. You may pass an on-curve wallet address or an off-curve PDA as the trader authority. The fee payer must not be the Phoenix onboarder key. Your app should await confirmation for the returned signature. ```ts TypeScript theme={null} import { createPhoenixClient, type Authority, type RegisterIxInstruction, } from "@ellipsis-labs/rise"; import { createKeyPairSignerFromBytes } from "@solana/signers"; import { AccountRole, address, appendTransactionMessageInstructions, compileTransaction, createSolanaRpc, createTransactionMessage, getBase64EncodedWireTransaction, partiallySignTransaction, pipe, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, type Blockhash, } from "@solana/kit"; declare const traderAuthority: Authority; declare const feePayerKeypairBytes: Uint8Array; const toInstruction = (ix: RegisterIxInstruction) => ({ programAddress: address(ix.programId), accounts: ix.keys.map((account) => ({ address: address(account.pubkey), role: account.isSigner ? account.isWritable ? AccountRole.WRITABLE_SIGNER : AccountRole.READONLY_SIGNER : account.isWritable ? AccountRole.WRITABLE : AccountRole.READONLY, })), data: Uint8Array.from(ix.data), }); const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", exchangeMetadata: { stream: false }, }); const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com"); const feePayerSigner = await createKeyPairSignerFromBytes(feePayerKeypairBytes); const txFeePayer = feePayerSigner.address as Authority; const latestBlockhash = await rpc .getLatestBlockhash({ commitment: "finalized" }) .send(); const built = await client.api.exchange().buildRegisterIxs({ traderAuthority, txFeePayer, maxPositions: 128, }); const message = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayer(txFeePayer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash( { blockhash: latestBlockhash.value.blockhash as Blockhash, lastValidBlockHeight: latestBlockhash.value.lastValidBlockHeight, }, tx, ), (tx) => appendTransactionMessageInstructions( built.instructions.map(toInstruction), tx, ), ); const signed = await partiallySignTransaction( [feePayerSigner.keyPair], compileTransaction(message), ); const response = await client.api.exchange().sendRegisterIxs({ transaction: getBase64EncodedWireTransaction(signed), traderAuthority, txFeePayer, maxPositions: 128, traderPdaIndex: 0, traderSubaccountIndex: 0, }); console.log(response.signature, response.traderPda); ``` ```rust Rust theme={null} use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use phoenix_rise::{ ApiInstructionResponse, BuildRegisterIxsRequest, PhoenixHttpClient, SendRegisterIxsRequest, }; use solana_commitment_config::CommitmentConfig; use solana_instruction::{AccountMeta, Instruction}; use solana_keypair::read_keypair_file; use solana_pubkey::Pubkey; use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_signer::Signer; use solana_transaction::Transaction; use solana_transaction::versioned::VersionedTransaction; fn to_instruction(ix: &ApiInstructionResponse) -> Result> { Ok(Instruction { program_id: ix.program_id.parse()?, accounts: ix.keys.iter().map(|account| { Ok(AccountMeta { pubkey: account.pubkey.parse()?, is_signer: account.is_signer, is_writable: account.is_writable, }) }).collect::, Box>>()?, data: ix.data.clone(), }) } let http = PhoenixHttpClient::new_public("https://perp-api.phoenix.trade")?; let rpc = RpcClient::new_with_commitment( "https://api.mainnet-beta.solana.com".to_string(), CommitmentConfig::confirmed(), ); let fee_payer_keypair = read_keypair_file("/path/to/fee-payer-keypair.json")?; let trader_authority = "PDA_OR_AUTHORITY_PUBKEY".parse::()?; let tx_fee_payer = fee_payer_keypair.pubkey(); let built = http.exchange().build_register_ixs(&BuildRegisterIxsRequest { trader_authority: trader_authority.to_string(), tx_fee_payer: tx_fee_payer.to_string(), max_positions: Some(128), }).await?; let ixs = built.instructions .iter() .map(to_instruction) .collect::, _>>()?; let recent_blockhash = rpc.get_latest_blockhash().await?; let mut tx = Transaction::new_with_payer(&ixs, Some(&tx_fee_payer)); tx.try_partial_sign(&[&fee_payer_keypair], recent_blockhash)?; let transaction = BASE64_STANDARD.encode(bincode::serialize( &VersionedTransaction::from(tx), )?); let response = http.exchange().send_register_ixs(&SendRegisterIxsRequest { transaction, trader_authority: trader_authority.to_string(), tx_fee_payer: tx_fee_payer.to_string(), max_positions: Some(128), trader_pda_index: Some(0), trader_subaccount_index: Some(0), }).await?; println!("submitted {}", response.signature); ``` Full runnable examples: * [TypeScript builder onboarding transaction](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/10-builder-onboarding-tx.ts) * [Rust builder onboarding transaction](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/builder_onboarding_tx.rs) ## After onboarding Both paths activate the trader's default cross-margin account at `traderPdaIndex = 0` and `traderSubaccountIndex = 0`. The cross account's `max_positions` value is set during registration and must be between `32` and `128` inclusive. Next steps: * Learn account indexes, isolated accounts, position authority, and trader-state subscriptions in [Accounts](/sdk/accounts). * Fund the account in [Collateral](/sdk/collateral). * Send orders in [Orders](/sdk/orders) and monitor account health in [Margin](/sdk/margin). # Rise SDK Source: https://docs.phoenix.trade/sdk/rise Developer SDKs for Phoenix perpetuals in TypeScript and Rust. Rise is the developer-facing SDK surface for Phoenix perpetual futures. Use it after you understand the protocol concepts in the Phoenix docs and want to build an app, service, bot, or on-chain integration that reads Phoenix state and sends Phoenix instructions. The SDK sources live in the public repo: [github.com/Ellipsis-Labs/rise-public](https://github.com/Ellipsis-Labs/rise-public). The SDK currently ships as: * `rise/ts` - TypeScript SDK with HTTP route clients, a unified Phoenix client, exchange metadata caching, instruction builders, order-packet helpers, WebSocket adapters, trader-state stores, and margin helpers. * `rise/rust` - Rust workspace centered on the `phoenix-rise` crate, with typed HTTP and WebSocket clients, `PhoenixMetadata`, `PhoenixTxBuilder`, trader-state containers, margin math, and low-level instruction builders. ## Install ### TypeScript ```bash npm theme={null} npm add @ellipsis-labs/rise ``` ```bash bun theme={null} bun add @ellipsis-labs/rise ``` ```bash pnpm theme={null} pnpm add @ellipsis-labs/rise ``` ### Rust ```bash Default SDK theme={null} cargo add phoenix-rise ``` ```bash API only theme={null} cargo add phoenix-rise --no-default-features --features api ``` ```bash On-chain CPI theme={null} cargo add phoenix-rise --no-default-features --features cpi ``` ## Create A Client ```ts TypeScript theme={null} import { createPhoenixClient } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", ws: { connectMode: "eager" }, exchangeMetadata: { stream: true }, }); ``` ```rust Rust theme={null} use phoenix_rise::{PhoenixHttpClient, PhoenixMetadata}; let http = PhoenixHttpClient::new_from_env()?; let exchange = http.get_exchange().await?.into(); let metadata = PhoenixMetadata::new(exchange); ``` For package setup and runnable examples, use the public SDK sources: * [TypeScript README](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/README.md) * [TypeScript examples](https://github.com/Ellipsis-Labs/rise-public/tree/master/ts/examples) * [Rust README](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/README.md) * [Rust SDK examples](https://github.com/Ellipsis-Labs/rise-public/tree/master/rust/sdk/examples) ## Which Client To Use | Task | TypeScript | Rust | | ---------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------- | | HTTP snapshots, history, and transaction-builder routes | `client.api` or `PhoenixHttpClient` | `PhoenixHttpClient` | | Exchange params, market metadata, PDA context, and order-packet sizing | `client.exchange` cache | `PhoenixMetadata` | | Live market, orderbook, exchange, and trader streams | `client.streams`, `client.marketData()`, `client.orderbooks()` | `PhoenixWSClient` plus typed containers | | Trader snapshots and deltas | `createTraderStateStore(client)` | `Trader::apply_update(...)` | | Build Phoenix instructions | `client.ixs` | `PhoenixTxBuilder` | | Margin and account-health calculations | `createMarginCalculator(...)` and trader-state `marginInputs()` | `TraderPortfolio::compute_margin(...)` | Prefer the exchange cache for market parameters. In TypeScript, wait for `client.exchange.ready()` and read markets with `client.exchange.market(symbol)` or `client.exchange.marketMetadata(symbol)`. In Rust, build `PhoenixMetadata` from `http.get_exchange()` and update it with live market stats through `metadata.apply_market_stats(&stats)`. Prefer trader-state primitives for snapshots and deltas. In TypeScript, `createTraderStateStore(client)` maintains local trader resources and exposes `marginInputs()`. In Rust, `Trader::apply_update(&msg)` applies `traderState` snapshots and deltas to the local container. ## First Reads ```ts TypeScript theme={null} const snapshot = await client.exchange.ready(); const sol = client.exchange.market("SOL"); const solMetadata = client.exchange.marketMetadata("SOL"); const trader = await client.api .traders() .getTraderStateSnapshot("AUTHORITY_PUBKEY", { traderPdaIndex: 0 }); console.log(snapshot.markets.length, sol?.assetId, solMetadata?.logoUri); console.log(trader.snapshot.subaccounts); ``` ```rust Rust theme={null} use phoenix_rise::{PhoenixHttpClient, PhoenixMetadata}; use solana_pubkey::Pubkey; use std::str::FromStr; let http = PhoenixHttpClient::new_from_env()?; let exchange = http.get_exchange().await?.into(); let metadata = PhoenixMetadata::new(exchange); let authority = Pubkey::from_str("AUTHORITY_PUBKEY")?; let sol = metadata.get_market("SOL"); let trader = http.traders().get_trader(&authority).await?; println!("{:?} {}", sol.map(|m| &m.symbol), trader.len()); ``` Runnable references: * [TypeScript HTTP client example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/01-http-client.ts) * [TypeScript unified client example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/phoenix-client-example.ts) * [Rust HTTP client example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/http_client.rs) * [Rust Phoenix client example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/phoenix_client.rs) ## Reading Order 1. [Auth](/sdk/auth) - optional for most public reads, required for referral onboarding and notifications. 2. [Trader Onboarding](/sdk/register) - create or activate a trader account. 3. [Accounts](/sdk/accounts) - understand authority, position authority, cross accounts, isolated accounts, and trader-state streams. 4. [Collateral](/sdk/collateral) - move USDC into and out of Phoenix trader accounts. 5. [Exchange Data](/sdk/markets) - exchange cache, market metadata, L2 orderbooks, market stats, and market calendars. 6. [WS/API Best Practices](/sdk/ws-api-best-practices) - when to stream, when to poll, and how to avoid rate-limit and drift issues. 7. [Orders](/sdk/orders) - build order instructions and manage stop losses and conditionals. 8. [Margin](/sdk/margin) - join market prices, compute account health, margin, and liquidation estimates. 9. [On-Chain Programs](/sdk/on-chain-programs) and [LiteSVM Testing](/sdk/litesvm-testing) - program integrations and local tests. ## REST, WebSocket, and RPC Use REST for startup snapshots, historical data, and server-built transaction workflows. Use WebSocket streams for current market state, exchange deltas, orderbooks, and trader state. Use Solana RPC for blockhashes, simulation, transaction submission, confirmation, and custom account reads. The usual production pattern is: 1. Fetch HTTP snapshots on startup. 2. Subscribe to WebSocket streams. 3. Apply exchange and trader deltas locally through SDK caches. 4. Build and sign instructions with the user's wallet or signer service. 5. Confirm transactions through Solana RPC. 6. Reconcile from trader-state streams, falling back to fresh HTTP snapshots after reconnects or sequence gaps. # WS/API Best Practices Source: https://docs.phoenix.trade/sdk/ws-api-best-practices Use Phoenix REST, WebSocket, and RPC surfaces without drifting state or hitting rate limits. Use REST for snapshots, historical reads, and server-built transaction workflows. Use WebSockets for live exchange, market, orderbook, and trader-state updates. Use Solana RPC for blockhashes, simulation, transaction submission, confirmation, and custom account reads. ## Default Pattern 1. Fetch startup snapshots over REST. 2. Start WebSocket subscriptions for exchange metadata, market stats, orderbooks, and trader state. 3. Apply snapshots and deltas through SDK primitives. 4. Build instructions from the exchange cache. 5. Sign and send through the user's wallet or signer service. 6. Reconcile from trader-state streams; refetch REST snapshots after reconnects or sequence gaps. ```ts TypeScript theme={null} import { createPhoenixClient, createTraderStateStore } from "@ellipsis-labs/rise"; const client = createPhoenixClient({ apiUrl: "https://perp-api.phoenix.trade", rpcUrl: "https://api.mainnet-beta.solana.com", ws: { connectMode: "eager" }, exchangeMetadata: { stream: true }, }); await client.exchange.ready(); const traderState = createTraderStateStore(client); const trader = traderState.resource({ authority: "AUTHORITY_PUBKEY", traderPdaIndex: 0, }); const release = trader.retain(); await trader.ready(); ``` ```rust Rust theme={null} use phoenix_rise::api::{PhoenixHttpClient, PhoenixMetadata, PhoenixWSClient, Trader, TraderKey}; use solana_pubkey::Pubkey; let http = PhoenixHttpClient::new_from_env()?; let exchange = http.get_exchange().await?.into(); let mut metadata = PhoenixMetadata::new(exchange); let authority: Pubkey = "AUTHORITY_PUBKEY".parse()?; let trader_key = TraderKey::from_authority(authority); let mut trader = Trader::new(trader_key.clone()); let ws = PhoenixWSClient::new_from_env()?; let (mut trader_rx, _handle) = ws.subscribe_to_trader_state(&authority)?; ``` ## Rate Limits Most public market and trader reads do not require auth, but authenticated sessions receive higher API limits. Auth is required for referral-code onboarding and notifications. To avoid rate-limit pressure: * Prefer WebSocket streams for frequently changing state. * Share one process-level Phoenix client where possible. * Share one subscription per market or trader and fan out locally. * Cache exchange params with `client.exchange` or `PhoenixMetadata`. * Cache trader state with `createTraderStateStore(client)` or `Trader::apply_update(...)`. * Use REST polling for slow reconciliation, not frame-by-frame UI updates. * Back off on `429` responses and retry after the server-provided delay when available. ## Snapshots And Deltas Do not hand-roll snapshot/delta application unless you are building SDK internals. In TypeScript: * `client.exchange` bootstraps from API or RPC, applies exchange WebSocket snapshots and deltas, and emits cache events. * `createTraderStateStore(client)` applies trader-state snapshots and deltas and exposes derived positions, orders, triggers, history, and `marginInputs()`. * `client.marketData()` combines all-mids, market-stats, and mark-price streams into per-symbol rows. In Rust: * `PhoenixMetadata::new(exchange)` creates the exchange cache from an HTTP snapshot. * `metadata.apply_market_stats(&stats)` updates market mark-price inputs used by margin math. * `Trader::apply_update(&msg)` applies trader-state WebSocket updates to a local trader container. * `L2Book::apply_update(&msg)` maintains an orderbook container. ## Reconnects WebSocket clients can reconnect, but your application still needs a reconciliation policy. After a reconnect, sequence gap, or prolonged disconnect: * Refresh exchange metadata over REST or wait for a fresh exchange snapshot. * Refetch or await a fresh trader-state snapshot before showing account health as final. * Drop stale L2 book state unless the SDK resource confirms it has been resynced. * Recompute margin only after both trader state and market prices are current. ## Notifications Notifications are an authenticated workflow. Use a Phoenix auth session when subscribing to user-specific notifications over REST or WebSocket. Public market streams should stay unauthenticated unless your integration needs authenticated API limits for the rest of its traffic. References: * [Auth](/sdk/auth) * [Exchange Data](/sdk/markets) * [Accounts](/sdk/accounts) * [Orders](/sdk/orders) * [Margin](/sdk/margin) * [WebSocket API](/api/websocket) * [TypeScript WebSocket example](https://github.com/Ellipsis-Labs/rise-public/blob/master/ts/examples/phoenix-ws-example.ts) * [Rust market-stats example](https://github.com/Ellipsis-Labs/rise-public/blob/master/rust/sdk/examples/subscribe_market_stats.rs)