Solaris AISolaris AI FlowDocs
Node ReferenceDeFi

Phoenix

Trade Solana perps on Phoenix (Ellipsis Labs) and read markets, orderbook, and trader state.

View as Markdown

Phoenix is the Solana perpetuals DEX from Ellipsis Labs. The Phoenix node reads market and account data, places trades, and moves collateral from your connected Privy wallet.

Every trading and collateral operation runs in paper mode by default: it validates and simulates against the live market but broadcasts nothing until you explicitly flip the node to Live. So you can build and test a whole strategy before any real funds move.

Quick start (paper)

  1. Add a Phoenix node and pick a trading op, e.g. Place Market Order.
  2. Set symbol (e.g. SOL), side (Bid = buy, Ask = sell), baseUnits (size), and the required price cap priceLimitUsd.
  3. Run it. The node stays in paper mode, estimates your fill against the live order book, and returns a simulated result (simulated: true, signature: "PAPER"). Nothing is signed or broadcast.
  4. When you want real trades, flip the node's PAPER / LIVE switch and activate Phoenix Flight access once (see Going live).

You don't need wallet activation, funds, or a credential to paper-test.

What each operation needs

Operation typeWhat it needs
Market data reads (getMarkets, getOrderbook, candles, funding)Nothing. Public data, no wallet or credential.
Account reads (getTraderState, history, PnL)Your connected wallet address (read-only, no signature, no activation).
Trading & collateral in paper modeJust be signed in. No activation, funds, or signature.
Trading & collateral in Live modeA wallet with Phoenix Flight access (one-time activation below) plus USDC collateral.

Per-op details are in Operations.

Paper trading (default)

Signed Phoenix nodes show a PAPER / LIVE switch on the node card. Read-only ops don't show it, they never broadcast. New nodes start in paper.

Paper mode runs the exact same input validation as live, then stops before signing: no wallet is resolved, no Flight whitelist is checked, nothing is broadcast. A config error fails identically in both modes, so paper is a faithful dry run. What it returns depends on the op:

  • Limit / market orders estimate your fill by walking the live L2 order book (VWAP across price levels, capped at your limit or price cap). This accounts for depth and slippage, so it is stricter than a top-of-book fill on large orders. When the relevant book side is empty it falls back to the mid.
  • Post-only orders check whether the price would rest as a maker or be rejected for crossing the spread, exactly as Live would reject it on-chain. You never get a false "resting" result that dies the instant you go live.
  • Stop-loss / conditional / cancel ops preview the trigger or cancellation they would submit.
  • Collateral / subaccount ops preview the USDC movement.

Flipping a node to LIVE opens a confirmation dialog (real funds move). Flipping back to PAPER is instant. The first time a Live signed op runs in a workflow, you are prompted once to authorize wallet signing; paper ops never prompt.

Simulated outputs

Every paper result carries simulated: true and signature: "PAPER" instead of a real tx hash, plus an op-specific preview field:

Op(s)Preview fieldContents
placeLimitOrder, placeMarketOrderfill (or null), plus optional wouldRest / noteEstimated filledBaseUnits, avgPriceUsd, notionalUsd, feeUsd (taker + builder fee), partial, priceCapped, usedFallbackMid. A limit order's unfilled remainder is reported under wouldRest; if nothing fills within the cap, fill is null and note explains why.
placePostOnlyOrderwouldRest, or the node failsIf the maker price would NOT cross, returns fill: null + wouldRest. If it WOULD cross (a buy at/above the best ask, a sell at/below the best bid), the node fails with a cross-rejection error, exactly as a live post-only is rejected on-chain.
placeStopLoss, placePositionConditionalOrderwouldPlaceTriggerThe trigger(s) that would be placed (kind, side, prices, direction, order kind, and size for conditionals).
cancelStopLoss, cancelOrdersById, cancelAllwouldCancelWhat would be cancelled (kind: "stopLoss" | "ordersById" | "all", plus the targets).
depositCollateral, withdrawCollateralwouldMove{ direction: "deposit" | "withdraw", amountUsdc }.
transferCollateral, transferCollateralChildToParent, syncParentToChildwouldMove{ kind, srcSubaccountIndex, dstSubaccountIndex, amountUsdc? } (no amountUsdc for sync).

Example paper fill (placeMarketOrder, buy 10 SOL against a book whose best ask is 84.90):

{
  "success": true,
  "operation": "placeMarketOrder",
  "simulated": true,
  "signature": "PAPER",
  "symbol": "SOL",
  "fill": {
    "side": "Bid",
    "requestedBaseUnits": "10",
    "filledBaseUnits": "10",
    "avgPriceUsd": "84.9",
    "notionalUsd": "849",
    "feeUsd": "0.38205",
    "partial": false,
    "priceCapped": false,
    "usedFallbackMid": false
  }
}

Going live: wallet activation

Phoenix Flight is in private beta, so each wallet must be activated once before it can place Live trades or move collateral. Paper and read-only ops never need this, so a fresh wallet can paper-test freely.

Activation happens inline in the node config dialog, not in Connections and not on the node card:

  1. Open a Phoenix node and pick a signed op (e.g. placeLimitOrder).
  2. Flip it to Live. If the wallet isn't activated yet, an Activate Phoenix Flight access panel appears at the bottom.
  3. Choose your code type (access or referral), paste the code, and click Activate.

You can use either an access code or a referral code; a valid referral code works on its own, you don't need an allowlist invite first. Request a code at x.com/PhoenixTrade.

Once activated, the panel disappears for good across every Phoenix node and workflow for that wallet, and live signed ops broadcast without prompting for a code again. No code is ever stored on the node, in workflow exports, or in a credential.

You also need USDC collateral on Phoenix before placing orders. Use depositCollateral (or fund directly at phoenix.trade); both deposit and withdraw run a balance precheck and fail fast with a clear message if you're short.

Operations

Market data (read-only)

OperationDescriptionKey inputsSigns?
getMarketsList all available Phoenix perp markets(none)No
getMarketGet a single market's fees, risk, and funding cadencesymbolNo
getOrderbookGet L2 orderbook depthsymbolNo
getCandlesOHLCV candle history for a marketsymbol, timeframe, optional startTime/endTime/limitNo
getMarketFillsPublic trade tape for a marketsymbol, optional limit/cursorNo
getMarketStatsHistoryOpen interest, fees, mark/spot price over timesymbol, optional timeframe/startTime/endTime/limitNo
getFundingOverviewFunding-rate series across all marketsoptional startTime/endTime/perMarketLimitNo
getFundingRateHistoryFunding-rate series for one marketsymbol, optional startTime/endTime/limitNo

getMarkets: Lists every perp market live on Phoenix in a single call, each with its fee schedule, risk/leverage tiers, and funding cadence. Takes no inputs. Use it to discover what's tradable, populate a market picker, or loop a workflow over every symbol. The raw markets array comes back under data.

getMarket: The same per-market detail as one row of getMarkets, but scoped to a single symbol you already know. Cheaper than pulling the whole list when you only care about one market. Returns that market's taker/maker fees, risk parameters, and funding info under data, with symbol echoed back.

getOrderbook: The current L2 order-book depth for symbol: aggregated bid and ask levels (price + size) plus the mid price. This is the exact book the paper-trading simulator walks to estimate fills. Use it to check liquidity and spread before trading, or to feed a pricing decision into an upstream AI/logic node.

getCandles: OHLCV candle history for symbol at a required timeframe (1m, 5m, 15m, 1h, 4h, 1d, etc.), with an optional startTime/endTime window and limit. Use it for charting, technical indicators, or backtesting logic. Times go on the wire as Unix seconds; the executor converts whatever format you supply.

getMarketFills: The public trade tape for symbol: the most recent fills that printed on the market, from all traders, newest first. Paginates by limit/cursor (no time window). Use it to watch live order flow or compute recent traded volume.

getMarketStatsHistory: A time series of market-level statistics for symbol: open interest, fees, and mark/spot price over time. Optional timeframe, time window, and limit. Use it to track how a market's open interest or basis evolves, e.g. to detect crowding.

getFundingOverview: Funding-rate history for all markets at once, returning one series per market so you don't have to loop symbol-by-symbol. Takes no symbol. perMarketLimit caps the number of points in each series (this is deliberately a different field from limit). Use it to scan funding across the whole venue, e.g. to find the richest carry.

getFundingRateHistory: Funding-rate history for a single symbol over an optional time window. Use it when you only care about one market and want finer control than the venue-wide overview gives.

Account (read-only, uses your connected wallet)

OperationDescriptionKey inputsSigns?
getTraderStateCollateral, positions, open orders, triggers (one snapshot)(none)No
getTraderOrderHistoryPaginated order history (V2: includes intended/placed/currentState/fills aggregates)optional symbol/limit/cursor/startTime/endTimeNo
getTraderTradeHistoryPaginated fills history (V2 endpoint)optional symbol/limit/cursorNo
getTraderCollateralHistoryPaginated deposits/withdrawals/transfersoptional limit/cursorNo
getTraderFundingHistoryPaginated funding paymentsoptional symbol/startTime/endTime/limit/cursor/resolutionNo
getTraderPnlRealized + unrealized PnL series over timeresolution, optional startTime/endTime/limitNo
getTraderPortfolioValuesPortfolio value series over timeresolution, optional startTime/endTime/limitNo
getTraderMarketPnlPnL per marketresolution, optional symbol/startTime/endTime/limitNo
getNotificationsPhoenix-side fill / liquidation / SL execution notificationsoptional limit/cursorNo

These read your own account using the workflow owner's Privy wallet address (derived server-side, never client input). No signature and no whitelist are needed.

getTraderState: A single live snapshot of your Phoenix account: collateral, open positions, resting orders, and active triggers across all subaccounts. If the wallet has never opened a Phoenix account it returns data: null and hasTraderAccount: false instead of erroring, so downstream branches can test {phoenixResponse.hasTraderAccount} without try/catch. This is the canonical "what do I hold right now" read, and the source you pull orderSequenceNumber / priceTicks from when cancelling orders by id.

getTraderOrderHistory: Paginated history of your orders (the V2 endpoint), including intended vs placed vs current state and per-order fill aggregates. Optional symbol filter, time window, and limit/cursor. Use it to audit what you submitted and how each order resolved.

getTraderTradeHistory: Paginated history of your executed fills (the V2 endpoint). Paginates by limit/cursor with an optional symbol filter and no time window. Use it for realized-trade accounting or to compute your average entry on a position.

getTraderCollateralHistory: Paginated record of your collateral movements: deposits, withdrawals, and inter-subaccount transfers. limit/cursor only. Use it to reconcile balances or build a funding ledger for a strategy.

getTraderFundingHistory: Paginated history of the funding payments you've paid or received, with optional symbol, time window, and a resolution bucket. Use it to attribute funding cost (or income) to a position over time.

getTraderPnl: Your combined realized + unrealized PnL as a time series. Requires a resolution (bucket size like 1h or 1d); optional time window and limit. Use it to chart account performance over time.

getTraderPortfolioValues: Your total portfolio value (account equity) as a time series; also requires resolution. Like getTraderPnl but tracks equity rather than PnL, so it's what you'd plot as an equity curve.

getTraderMarketPnl: PnL broken down per market, requiring resolution, with optional symbol to scope to one market. Use it to see which markets are making or losing you money.

getNotifications: Phoenix-side notifications for your account: fills, liquidations, and stop-loss executions. limit/cursor only. Use it to react to events (e.g. send an alert on liquidation) without polling positions yourself.

Trading (signed)

OperationDescriptionKey inputsSigns?
placeLimitOrderResting limit ordersymbol, side, priceUsd, baseUnitsYes
placeMarketOrderTake liquidity up to a price capsymbol, side, priceLimitUsd, baseUnitsYes
placePostOnlyOrderMaker-only limit (rejects on cross)symbol, side, priceUsd, baseUnitsYes
placeStopLossBare stop-loss / take-profit tied to a marketsymbol, side, triggerPrice, executionPrice, executionDirection, orderKindYes
cancelStopLossCancel an existing stop-loss / TPsymbol, executionDirectionYes
placePositionConditionalOrderUp to two greater/less triggers attached to a positionsymbol, conditionalTriggers (JSON), optional baseUnitsYes
cancelOrdersByIdCancel specific resting orderssymbol, orderIdsYes
cancelAllCancel every resting order on the symbolsymbolYes

placeLimitOrder: Posts a standard limit order from your side (Bid = buy/long, Ask = sell/short), at priceUsd, for baseUnits of the base asset. It immediately fills against any resting orders priced better than yours, then rests the remainder at your price as a maker. This is the default order type for most strategies. In paper mode the fill is estimated from the live book and any unfilled remainder is reported under wouldRest.

placeMarketOrder: Takes liquidity immediately, walking the book from the best price until your baseUnits is filled, but never past the required priceLimitUsd worst-price cap. The cap is mandatory: the node refuses to submit without it, so a thin or fast-moving book can't fill you at a runaway price. Use it when getting filled matters more than the spread you pay.

placePostOnlyOrder: A maker-only limit order. Same inputs as placeLimitOrder, but it will never take liquidity: if your price would cross the spread (a buy at or above the best ask, a sell at or below the best bid) Phoenix rejects it on-chain instead of filling. Use it to guarantee you post as a maker and never pay taker fees. In paper mode a crossing post-only fails the node, exactly mirroring the live rejection, so you don't get a false "resting" result.

placeStopLoss: Places a standalone stop-loss or take-profit tied to a symbol (not attached to a specific order). triggerPrice arms it; executionDirection (GreaterThan / LessThan) sets whether it fires when price rises above or falls below the trigger; when it fires it submits at executionPrice as either IOC (fill what it can immediately at that worst-case price, cancel the rest) or Limit (rest at that price as a maker). side is the direction of the protective order. Use it to auto-exit a position at a threshold.

cancelStopLoss: Removes an existing stop-loss / take-profit on symbol, identified by the executionDirection you placed it with. Use it to clear a trigger before replacing it, or once the position it protected is closed.

placePositionConditionalOrder: Attaches up to two price triggers to a position in one instruction via the conditionalTriggers JSON: a greater bucket (fires when price rises through a level) and/or a less bucket (fires when price falls through one). Each entry carries its own tradeSide, orderKind (IOC/Limit), triggerPrice, and executionPrice. Optional baseUnits sizes the order; omit it to size to the full position. Use it for bracket-style exits, e.g. a take-profit above plus a stop below, set together.

cancelOrdersById: Cancels specific resting orders identified by orderIds, a JSON array of { price | priceTicks, orderSequenceNumber } entries pulled from a prior getTraderState snapshot. Use it for surgical cancellation when you don't want to wipe the whole book. See Cancel-by-id price precision for when to use price vs the tick-perfect priceTicks.

cancelAll: Cancels every resting order you have on symbol in a single instruction. Use it as a fast reset before re-quoting, or as a kill-switch step in a rebalance flow.

Collateral & subaccount management (signed)

OperationDescriptionKey inputsSigns?
depositCollateralDeposit USDC into your Phoenix trader accountamountUsdcYes
withdrawCollateralWithdraw USDC from your Phoenix trader accountamountUsdcYes
transferCollateralMove USDC between two subaccounts under the same walletsrcSubaccountIndex, dstSubaccountIndex, amountUsdcYes
transferCollateralChildToParentSweep an isolated subaccount's full balance back to cross maintraderSubaccountIndex (the child)Yes
syncParentToChildRefresh state on an isolated subaccount from the parenttraderSubaccountIndex (the child)Yes

No Flight builder fee applies to any collateral op, even live: moving USDC never has Solaris AI take a cut.

depositCollateral: Moves amountUsdc of USDC from your wallet into your Phoenix trader account at the resolved (marginType, traderSubaccountIndex). Before broadcasting, a precheck reads your wallet's USDC balance and fails fast (reporting actual vs required) if you're short. The first deposit to a fresh subaccount auto-registers it. You need collateral here before you can place any orders.

withdrawCollateral: Pulls amountUsdc of USDC out of your Phoenix account back to your wallet. A precheck reads effectiveCollateralForWithdrawals (your withdrawable balance after open positions, unrealized PnL, funding, and margin) and refuses if it's below the request, so you can branch on that error instead of hitting a cryptic on-chain failure. Use it to take profit or free up capital.

transferCollateral: Moves amountUsdc between two subaccounts under the same wallet, from srcSubaccountIndex to dstSubaccountIndex (both 0-100, must differ). The source must already hold collateral; a missing destination is auto-registered (Cross for subaccount 0, Isolated otherwise). Use it to rebalance capital between strategies without round-tripping through your wallet.

transferCollateralChildToParent: Sweeps an Isolated subaccount's entire balance back to the Cross main account (subaccount 0); traderSubaccountIndex is the child being emptied. Use it to wind down an isolated strategy and reclaim its collateral. If the parent (subaccount 0) doesn't exist yet, it's auto-registered as part of the same transaction.

syncParentToChild: Refreshes an Isolated child subaccount's view of state from the Cross parent; traderSubaccountIndex is the child. It's idempotent (re-syncing identical state is a no-op revert). Use it to reconcile a child after parent-side changes.

The Signs? column above describes Live mode. In Paper mode (the default for every op marked "Yes") nothing is signed or broadcast; the op is simulated instead.

All trading + collateral signed operations (except transfer/sync, which have their own subaccount routing) also accept marginType (default Cross) and, for Isolated margin, traderSubaccountIndex. See Margin and subaccount routing.

Configuration

FieldTypeRequired forDescription
symbolstringgetMarket, getOrderbook, and all trading / cancel opsBase asset symbol (e.g., SOL, BTC, ETH). A legacy -PERP suffix is stripped automatically for backwards compatibility. Also accepted as an optional market filter on getTraderOrderHistory, getTraderTradeHistory, getTraderFundingHistory, and getTraderMarketPnl (leave blank for all markets). Not used by getMarkets, getTraderState, depositCollateral, or withdrawCollateral.
sideBid | AskplaceLimitOrder, placeMarketOrder, placePostOnlyOrder, placeStopLossBid = buy / long, Ask = sell / short. Runtime also accepts templated/imported aliases buy / long and sell / short. (placePositionConditionalOrder instead sets tradeSide per entry inside conditionalTriggers.)
priceUsdstringplaceLimitOrder, placePostOnlyOrderLimit price in USD (e.g. 135.87). For post-only, this is the maker price the order rests at; it rejects rather than crossing if it would match a resting order on the opposite side.
priceLimitUsdstringplaceMarketOrderRequired price cap. Worst price you accept; market orders stop matching past this. Prevents slippage on thin books.
baseUnitsstringplaceLimitOrder, placeMarketOrder, placePostOnlyOrder (optional for placePositionConditionalOrder)Order size in the base asset (e.g. 0.25 for a 0.25 SOL position). Required for the three plain place ops; on placePositionConditionalOrder it's optional, omit it to size to the full position.
orderIdsJSON stringcancelOrdersByIdArray of { "price": <decimal USD>, "orderSequenceNumber": "<string>" }, e.g. [{"price": 135.87, "orderSequenceNumber": "1234"}]. price may be a JSON number or a base-10 decimal string ("135.87"); scientific notation and hex strings are rejected so a mis-typed "1e2" can't silently target tick 100. Pull from a prior getTraderState at data.snapshot.subaccounts[].orders[].orders[] (each row exposes priceUsd and orderSequenceNumber). Precision option: each entry may use priceTicks (positive integer string, e.g. "12345") instead of price. Pull from the snapshot's priceTicks field. Setting both price AND priceTicks on the same entry is rejected. See Cancel-by-id price precision for when to use which.
amountUsdcstringdepositCollateral, withdrawCollateral, transferCollateralUSDC amount as a base-10 decimal with at most 6 fractional digits (e.g. "1.5", "100", "0.000001"). "1.5" means 1.5 USDC (=1,500,000 base units), not 1,500,000 USDC. Scientific notation, hex, leading dots, and negatives are rejected; the parser converts string to bigint with no Number() so there's no float drift on large amounts. Supports template variables.
marginTypeCross | IsolatedTrading / cancel / deposit / withdraw signed ops (not transfer/sync)Margin mode for the trader account this op targets. Defaults to Cross. Locks irreversibly at first signed op for the (subaccount) pair. Phoenix doesn't allow changing margin type on an existing on-chain trader account. Cross is always routed to subaccount 0; Isolated requires traderSubaccountIndex in [1, 100].
traderSubaccountIndexnumber (1-100)Trading / cancel / deposit / withdraw with Isolated margin; ALSO transferCollateralChildToParent and syncParentToChild (as the child subaccount)Subaccount slot. For single-subaccount ops: required when marginType: "Isolated", must be omitted or 0 when Cross. For childToParent / sync: this IS the child subaccount, range 1-100. Subaccount 0 is reserved for Cross by SDK contract. Supports template variables. Phoenix supports up to 101 trader accounts per wallet: Cross at subaccount 0 plus Isolated at any of 1-100.
timeframestringgetCandles (required), getMarketStatsHistory (optional)Candle / stats resolution like 1m, 5m, 15m, 1h, 4h, 1d. Forwarded verbatim to Phoenix; whatever the upstream API accepts works.
startTime, endTimestringgetCandles, getMarketStatsHistory, getFundingOverview, getFundingRateHistory, getTraderOrderHistory, getTraderFundingHistory, getTraderPnl, getTraderPortfolioValues, getTraderMarketPnl (optional)ISO-8601, Unix seconds (10 digits), or Unix millis (13 digits). The executor normalizes per-op. Not accepted by getMarketFills, getTraderTradeHistory, getTraderCollateralHistory, or getNotifications (those endpoints paginate by limit / cursor only).
limitnumber/stringAll paginated read ops (optional)Max items per page. Phoenix enforces per-endpoint caps server-side.
cursorstringgetMarketFills, getTraderOrderHistory, getTraderTradeHistory, getTraderCollateralHistory, getTraderFundingHistory, getNotifications (optional)Pagination cursor from a prior response's nextCursor.
perMarketLimitnumber/stringgetFundingOverview (optional)Caps points per series (overview returns one series per market). Distinct from limit because FundingOverviewRequest uses a different key.
resolutionstringgetTraderPnl, getTraderPortfolioValues, getTraderMarketPnl (required), getTraderFundingHistory (optional)Aggregation bucket for time-series outputs (e.g. 1h, 1d). PnL / portfolio endpoints reject empty resolution.
triggerPrice, executionPricestring (USD decimal)placeStopLossTrigger price fires the SL; execution price is the resting limit (Limit kind) or worst acceptable fill (IOC kind). Converted to ticks per the market's tick size.
executionDirectionGreaterThan | LessThanplaceStopLoss, cancelStopLossTrigger comparison direction. Runtime also accepts aliases above/below/gt/lt/>/< to play nicely with template-driven AI workflows.
orderKindIOC | LimitplaceStopLossIOC cancels any unfilled remainder immediately at the execution price (worst case); Limit rests at the execution price as a maker order.
conditionalTriggersJSON stringplacePositionConditionalOrderArray of 1-2 entries {"bucket":"greater"|"less", "tradeSide":"Bid"|"Ask", "orderKind":"IOC"|"Limit", "triggerPrice":"<USD>", "executionPrice":"<USD>"}. At most one per bucket; setting bucket: "greater" twice is rejected.
srcSubaccountIndex, dstSubaccountIndexnumber (0-100)transferCollateralSource and destination subaccounts. Both required, must differ, both in [0, 100]. Source must already hold collateral; destination is auto-registered (Isolated for non-zero, Cross for 0) if it doesn't exist yet.

Invite codes are never configured as fields on the Phoenix node. Activation is a one-time wallet-level action handled via the inline panel described in Going live.

Using template variables

Most fields accept template variables (e.g. {aiResponse.data.targetPrice}): symbol, side, priceUsd, priceLimitUsd, baseUnits, orderIds, amountUsdc, marginType, traderSubaccountIndex, timeframe, startTime, endTime, limit, cursor, perMarketLimit, resolution, triggerPrice, executionPrice, executionDirection, orderKind, conditionalTriggers, srcSubaccountIndex, and dstSubaccountIndex.

A few of these (side, marginType, orderKind, executionDirection) are shown as fixed dropdowns in the canvas config rather than free-text boxes, so on the canvas you pick a value. To drive one from an upstream node, template it from a JSON-edited workflow instead (e.g. side as {aiResponse.suggestedSide} resolving to "Bid" or "Ask").

Numeric fields (priceUsd, priceLimitUsd, baseUnits) must be base-10 decimal strings (e.g. "135.87", "0.25"); scientific notation, hex, and leading dots are rejected. amountUsdc follows the same shape but caps fractional digits at 6 (USDC's on-chain decimal precision).

Time window inputs

Read ops that take a time window (getCandles, getMarketStatsHistory, getFundingOverview, getFundingRateHistory, getTraderOrderHistory, getTraderFundingHistory, getTraderPnl, getTraderPortfolioValues, getTraderMarketPnl) accept any of these formats for startTime / endTime:

  • ISO-8601 string, e.g. 2026-01-01T00:00:00Z
  • Unix seconds, 10-digit integer string, e.g. 1700000000
  • Unix millis, 13-digit integer string, e.g. 1700000000000 (floored to seconds)

The executor normalizes the format per endpoint, so you don't have to remember which one each uses. getMarketFills, getTraderTradeHistory, getTraderCollateralHistory, and getNotifications paginate by limit / cursor only and expose no time window.

Pagination

For ops with a cursor field, Phoenix returns nextCursor on each page. Template the value back in via {phoenixResponse.data.nextCursor} and re-run the node to fetch the next page. cursor is empty/absent on the first call. Same field name and shape across getMarketFills, getTraderOrderHistory, getTraderTradeHistory, getTraderCollateralHistory, getTraderFundingHistory, and getNotifications.

Output

Symbol-scoped market reads (getMarket, getOrderbook, getCandles, getMarketFills, getMarketStatsHistory, getFundingRateHistory) echo symbol; getMarkets and getFundingOverview omit it (they take no symbol):

{
  "success": true,
  "operation": "getOrderbook",
  "symbol": "SOL",
  "data": { /* raw Phoenix response */ }
}

getTraderState adds the wallet authority and hasTraderAccount:

{
  "success": true,
  "operation": "getTraderState",
  "authority": "7abc...",
  "data": null,
  "hasTraderAccount": false
}

Live signed place/cancel ops return a real signature plus status flags (in Paper mode the shape is different: see Simulated outputs):

{
  "success": true,
  "operation": "placeLimitOrder",
  "symbol": "SOL",
  "signature": "5JR...",
  "registeredThisCall": false,
  "nowWhitelisted": true
}
  • registeredThisCall: true means this broadcast bundled the register-trader instruction (the first signed op on a fresh subaccount). It's distinct from getTraderState.hasTraderAccount, which describes current on-chain state independent of this call.
  • nowWhitelisted: true is always true on success. Useful as a downstream gate like {phoenixResponse.nowWhitelisted}. Reference any field with the response name (default phoenixResponse), e.g. {phoenixResponse.signature} to forward the tx hash to a notification node.

Collateral ops return the same shape with amountUsdc in place of symbol. transferCollateral echoes both subaccount indices; transferCollateralChildToParent and syncParentToChild echo childSubaccountIndex.

Account-history reads use a nested envelope: outer data is our wrapper, inner data is Phoenix's paginated response, so you template the next page as {phoenixResponse.data.nextCursor}:

{
  "success": true,
  "operation": "getTraderOrderHistory",
  "authority": "7abc...",
  "data": { "data": [/* orders */], "nextCursor": "...", "hasMore": true }
}

Margin and subaccount routing

Phoenix supports two margin modes and up to 101 trader accounts per wallet:

  • Cross is always routed to subaccount 0 (one shared collateral pool backs every position in that subaccount). Picking marginType: "Cross" and omitting traderSubaccountIndex covers the typical single-strategy case.
  • Isolated silos each strategy in its own subaccount (1-100), with its own collateral pool, positions, and margin accounting. Pick marginType: "Isolated" and set traderSubaccountIndex to any number in 1-100. Each (margin, subaccount) combo is its own on-chain account that auto-registers on its first signed op.

You can mix them on the same wallet, e.g. a Cross BTC strategy on subaccount 0 plus an Isolated SOL trial on subaccount 5. Cross at a non-zero subaccount, or Isolated at subaccount 0, are rejected at publish time. Margin mode locks irreversibly at the first signed op for a subaccount: once registered as Cross or Isolated it stays that way, so switch by routing the next strategy to a different subaccount.

depositCollateral and withdrawCollateral target the resolved (margin, subaccount) pair. To fund an Isolated strategy on subaccount 5, set marginType: "Isolated", traderSubaccountIndex: 5 on the deposit node. The first deposit auto-registers subaccount 5 as Isolated and credits the USDC there, independent of subaccount 0.

Cancel-by-id price precision

When cancelling specific orders, you identify each one by price. Phoenix stores prices as integer "ticks," and converting a decimal USD price back to a tick uses floor math that can occasionally land on the tick next to your actual order, so the cancel misses. You have two ways to cancel:

  • price (USD): the simple path. Hand the node a decimal USD value (e.g. 135.87). Works cleanly for prices that aren't on a precision-fragile boundary.
  • priceTicks (string): the precision-safe path. Pull the order's exact on-chain tick from a getTraderState snapshot at data.snapshot.subaccounts[].orders[].orders[].priceTicks. The node converts it back to a price that is guaranteed to target the right tick, and refuses to submit if it can't, so you never cancel the wrong order.

Pick priceTicks for deterministic, tick-perfect cancellation (high-frequency or replace-on-cancel flows). Pick price for the simpler path when the order's price is whole-cents-flat. Each entry must use one or the other; setting both is rejected.

Common use cases

  • AI sentiment to a conditional placeLimitOrder on Phoenix
  • Cross-venue arbitrage: read Drift orderbook, place a hedge on Phoenix
  • Scheduled rebalancing: getTraderState to cancelAll to fresh limit orders
  • Scheduled collateral top-up: cron trigger to depositCollateral with a templated amountUsdc
  • Automated profit-take: run withdrawCollateral with your threshold amount and let the precheck gate it. The precheck reads effectiveCollateralForWithdrawals (which accounts for open positions, unrealized PnL, funding, and margin) and rejects with a clean "insufficient withdrawable" error, so you can route the workflow on that error. Note it's best-effort: state can move between precheck and broadcast, so a precheck-pass withdraw can still fail on-chain. Don't gate on getTraderState.data.snapshot.subaccounts[].collateral: it's raw per-subaccount collateral and does NOT account for positions/PnL/funding/margin.

Under the hood

You don't need any of this to use the node. It's here for the curious and for anyone debugging unexpected behavior.

Builder fees

Solaris AI is a registered Phoenix Flight builder, so order-placing instructions (placeLimitOrder, placeMarketOrder, placePostOnlyOrder, placePositionConditionalOrder) carry Solaris AI's builder code and earn a builder fee on the resulting taker fills (rate set server-side by Phoenix, currently 1 bp). Maker fills earn no builder fee, so a post-only order (maker-only) never generates one even though its instruction is still wrapped. Stop-loss, cancel, and collateral instructions are not order-placing, so they're never wrapped and never carry a fee. A test asserts the builder authority stays wired up so a future refactor can't silently drop it.

Reliability

  • placeMarketOrder will not submit without priceLimitUsd set, even though the SDK accepts uncapped market packets. The node treats that as user error and fails before broadcasting.
  • Each signed transaction is prepended with compute-budget instructions and a priority fee. The fee is estimated per broadcast from a live Helius getPriorityFeeEstimate (High level, keyed off the transaction's writable accounts) and clamped to a floor/ceiling (currently 40,000 to 1,000,000 micro-lamports/CU), so time-sensitive trades and cancels stay competitive under congestion without a fee spike draining the wallet. Estimating per build means a blockhash-expiry retry also re-prices instead of resubmitting the same fee. If the estimate is unavailable (RPC without the method, timeout, error), it falls back to the floor. Most ops use a 400k CU budget; deposit and withdraw chain more instructions and use 800k.
  • If a Live signed op throws, the engine does not automatically re-run the node, because a fresh attempt could duplicate an order or fund movement that may have actually landed (a thrown op is ambiguous about whether its transaction landed). The one exception is syncParentToChild, which is idempotent (re-syncing identical state is a no-op) and stays retryable. Paper ops are always retryable since they never broadcast; a transient order-book read just runs again.
  • Deposits and withdrawals run a balance precheck (wallet USDC for deposit, effectiveCollateralForWithdrawals for withdraw) and fail fast with a clear message rather than a cryptic on-chain error. The withdraw precheck is best-effort: on-chain state can move between precheck and broadcast.

How activation is checked

Activation is verified, never stored. The activation panel calls a server action that resolves your wallet via Privy, checks Phoenix's whitelist, and only consumes a code if you actually need one (an already-whitelisted wallet, or one that already has a Phoenix trader account from a prior referral activation, short-circuits without burning a code). At runtime the executor only reads activation state before a Live broadcast: it checks the whitelist, falls back to probing for an existing default trader account, and throws a clear "open the Phoenix node and click Activate" error if neither proves access. No transaction is broadcast in that case, and the executor never sees invite codes.

SDK coverage

The node wraps the Ellipsis Labs Rise SDK. These SDK surfaces are intentionally not exposed as node operations:

  • placeMultiLimitOrder, placeAttachedConditionalOrder, placeLimitOrderWithConditionals, cancelUpTo, cancelConditionalOrder: only practical via low-level instruction builders that need manual account resolution.
  • placeIsolatedLimitOrder / placeIsolatedMarketOrder: superseded by our two-step transferCollateral then placeLimitOrder flow, which has clearer node-level intent.
  • delegateTrader: multi-signer / copy-trading delegation, out of scope for typical workflows.
  • Escrow operations (create / accept / cancel request, grant / revoke permission): niche inter-trader transfers, not in current scope.
  • Notification ack endpoints: write ops on the notification feed; would need their own signing-op slot.
  • WebSocket / streaming surfaces: workflows run as one-shot HTTP calls, so long-lived subscriptions don't fit.

Next steps

  • Drift - compare perps surface
  • Jupiter - spot routing for hedging

On this page