Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.phoenix.trade/llms.txt

Use this file to discover all available pages before exploring further.

Access code vs referral code

These activation routes are not interchangeable:
  • Use POST /v1/invite/activate when you have an access code / allowlist code. Send that value as code.
  • Use POST /v1/referral/activate when you have a referral code. Send that value as referral_code. This route requires an authenticated user session for authority.
import { PhoenixHttpClient } from "@ellipsis-labs/rise";
import bs58 from "bs58";

const client = new PhoenixHttpClient({
  apiUrl: "https://perp-api.phoenix.trade",
  auth: true,
});

declare const authorityWallet: {
  publicKey: { toString(): string };
  signMessage(message: Uint8Array): Promise<Uint8Array>;
};
const authority = authorityWallet.publicKey.toString();

const activatedWithAccessCode = await client.invite().activateInvite({
  authority,
  code: "ACCESS_CODE",
});

const auth = client.auth()!;
const nonce = await auth.getWalletNonce(authority);
const signature = await authorityWallet.signMessage(
  new TextEncoder().encode(nonce.message)
);
await auth.loginWithWalletSignature(
  authority,
  bs58.encode(signature),
  nonce.nonce_id
);

const activatedWithReferral = await client.invite().activateReferral({
  authority,
  referral_code: "REF_CODE",
});
For the JWT lifecycle required by referral activation, see Auth. For a ready-to-run Rust version of this flow, see rust/sdk/examples/register_trader.rs.

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 activation and has max_positions = 128.
  • subaccount_index > 0 is an isolated account. Isolated accounts have max_positions = 1, so each account is intended for one isolated position.

Create isolated accounts

Activation 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.
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.
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.
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.
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.
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.