Skip to main content
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. 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.
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);
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::<Instruction>::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::<Result<Vec<_>, _>>()?,
            )
            .active_trader_buffer(
                exchange.keys().active_trader_buffer
                    .iter()
                    .map(|key| key.parse())
                    .collect::<Result<Vec<_>, _>>()?,
            )
            .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:

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.
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);
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<Instruction, Box<dyn std::error::Error>> {
    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::<Result<Vec<_>, Box<dyn std::error::Error>>>()?,
        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::<Pubkey>()?;
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::<Result<Vec<_>, _>>()?;

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:

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.
  • Fund the account in Collateral.
  • Send orders in Orders and monitor account health in Margin.