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

# Trader Onboarding

> Activate Phoenix trader accounts with or without a referral code.

<Warning>
  Do not build new integrations around `/v1/invite/activate` or `/v1/referral/activate`. These sponsored activation routes will be deprecated on June 30, 2026.
</Warning>

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 the user-controlled trader authority to sign.
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 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`.

## 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`.

<CodeGroup>
  ```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::<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:?}");
  ```
</CodeGroup>

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

<CodeGroup>
  ```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<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);
  ```
</CodeGroup>

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/examples/builder_onboarding_tx.rs)

## After onboarding

Both paths activate the trader's default cross-margin account at `traderPdaIndex = 0` and `traderSubaccountIndex = 0`. For account indexes and isolated subaccounts, see [Trader Accounts](/sdk/onboarding).
