Skip to main content
This guide walks through a complete private trade from scratch: one-time wallet, swap transaction, Jito routing, and commit.
You’ll need an API key to follow this guide. Contact the Vanish team via Discord to get one during onboarding.

Prerequisites

npm install @solana/web3.js tweetnacl
Base URL (Dev): https://core-api-dev.vanish.trade Base URL (Prod): https://core-api.vanish.trade Authentication: All endpoints require an x-api-key header.

1. Set Up the Client

const VANISH_URL = 'https://core-api.vanish.trade';

async function vanish(path: string, options?: RequestInit) {
  const res = await fetch(`${VANISH_URL}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.VANISH_API_KEY!,
      ...options?.headers,
    },
  });
  if (!res.ok) {
    throw new Error(`Vanish ${res.status}: ${await res.text()}`);
  }
  return res.json();
}

2. Sign Requests

import * as nacl from 'tweetnacl';
import { Keypair } from '@solana/web3.js';

function signMessage(message: string, keypair: Keypair): string {
  const sig = nacl.sign.detached(new TextEncoder().encode(message), keypair.secretKey);
  return Buffer.from(sig).toString('base64');
}

function readSignature(timestamp: string, keypair: Keypair) {
  return signMessage(
    `By signing, I hereby agree to Vanish's Terms of Service and agree to be bound by them (docs.vanish.trade/legal/TOS)\n\nDetails: read:${timestamp}`,
    keypair
  );
}

function tradeSignature(
  source: string, target: string, amount: string,
  loanSol: string, timestamp: string, jitoTip: string,
  keypair: Keypair
) {
  return signMessage(
    `By signing, I hereby agree to Vanish's Terms of Service and agree to be bound by them (docs.vanish.trade/legal/TOS)\n\nDetails: trade:${source}:${target}:${amount}:${loanSol}:${timestamp}:${jitoTip}`,
    keypair
  );
}

3. Execute a Private Trade

This example buys a token using native SOL via Jito routing (default). Vanish broadcasts automatically.
const SOL_MINT = '11111111111111111111111111111111';
const LOAN_SOL = '12000000'; // 0.012 SOL - unused amount is refunded
const JITO_TIP = '200000';   // 0.0002 SOL

async function executeTrade(
  userKeypair: Keypair,
  targetToken: string,
  amountLamports: string,
  unsignedSwapTx: string   // base64 unsigned tx from your DEX aggregator
) {
  const timestamp = Date.now().toString();

  // Step 1 - get a single-use wallet for this trade
  const { address: oneTimeWallet } = await vanish('/trade/one-time-wallet');

  // Step 2 - sign the Trade Signing Format with the user's wallet
  const userSignature = tradeSignature(
    SOL_MINT, targetToken, amountLamports,
    LOAN_SOL, timestamp, JITO_TIP,
    userKeypair
  );

  // Step 3 - submit. Vanish wraps the swap, broadcasts via Jito, returns tx_id
  const trade = await vanish('/trade/create', {
    method: 'POST',
    body: JSON.stringify({
      user_address:         userKeypair.publicKey.toBase58(),
      source_token_address: SOL_MINT,
      target_token_address: targetToken,
      amount:               amountLamports,
      swap_transaction:     unsignedSwapTx,   // must be unsigned
      one_time_wallet:      oneTimeWallet,
      loan_additional_sol:  LOAN_SOL,
      jito_tip_amount:      JITO_TIP,
      split_repay:          1,
      timestamp,
      user_signature:       userSignature,
      // omit prefer_non_jito to use Jito route (default)
    }),
  });

  // Step 4 - commit (required for every trade, regardless of outcome)
  const commit = await vanish('/commit', {
    method: 'POST',
    body: JSON.stringify({ tx_id: trade.tx_id }),
  });

  console.log('Status:', commit.status);
  console.log('Balance changes:', commit.balance_changes);
  return commit;
}
Always call /commit - even if the trade fails on-chain. Without it, the user’s balance remains in a pending state.

4. Non-Jito Variant (Self-Broadcast)

If you want to broadcast through your own RPC, pass prefer_non_jito:
import { Connection, VersionedTransaction } from '@solana/web3.js';

async function executeTradeNonJito(
  userKeypair: Keypair,
  targetToken: string,
  amountLamports: string,
  unsignedSwapTx: string,
  connection: Connection
) {
  const timestamp = Date.now().toString();
  const { address: oneTimeWallet } = await vanish('/trade/one-time-wallet');

  const userSignature = tradeSignature(
    SOL_MINT, targetToken, amountLamports,
    LOAN_SOL, timestamp, JITO_TIP,
    userKeypair
  );

  const trade = await vanish('/trade/create', {
    method: 'POST',
    body: JSON.stringify({
      user_address:         userKeypair.publicKey.toBase58(),
      source_token_address: SOL_MINT,
      target_token_address: targetToken,
      amount:               amountLamports,
      swap_transaction:     unsignedSwapTx,
      one_time_wallet:      oneTimeWallet,
      loan_additional_sol:  LOAN_SOL,
      jito_tip_amount:      JITO_TIP,
      split_repay:          1,
      timestamp,
      user_signature:       userSignature,
      prefer_non_jito: {
        compute_unit_price: '71429',
        compute_unit_limit: '1400000',
      },
    }),
  });

  // Broadcast the signed transaction yourself
  const txBytes = Buffer.from(trade.transaction, 'base64');
  const tx      = VersionedTransaction.deserialize(txBytes);
  const txId    = await connection.sendRawTransaction(tx.serialize(), {
    skipPreflight: false,
    maxRetries:    3,
  });
  await connection.confirmTransaction(txId, 'confirmed');

  const commit = await vanish('/commit', {
    method: 'POST',
    body: JSON.stringify({ tx_id: txId }),
  });

  return commit;
}

5. Check Balances

async function getBalances(userKeypair: Keypair) {
  const timestamp = Date.now().toString();
  const signature = readSignature(timestamp, userKeypair);

  return vanish('/account/balances', {
    method: 'POST',
    body: JSON.stringify({
      user_address: userKeypair.publicKey.toBase58(),
      timestamp,
      signature,
    }),
  });
  // Returns: [{ token_address, balance, program_id }, ...]
  // balance is in base units (lamports for SOL)
}

6. Recover Interrupted Flows

If your app crashes after a trade is submitted but before /commit is called, the user’s balance is held in a pending state. Poll /account/pending to recover:
async function recoverPendingCommits(userKeypair: Keypair) {
  const timestamp = Date.now().toString();
  const signature = readSignature(timestamp, userKeypair);

  const pending = await vanish('/account/pending', {
    method: 'POST',
    body: JSON.stringify({
      user_address: userKeypair.publicKey.toBase58(),
      timestamp,
      signature,
    }),
  });

  for (const action of pending) {
    const commit = await vanish('/commit', {
      method: 'POST',
      body: JSON.stringify({ tx_id: action.tx_id }),
    });
    // already_processed: true means it was already committed - safe to ignore
    if (!commit.already_processed) {
      console.log(`Resolved ${action.action_type}:`, commit.status);
    }
  }
}

Lamport Reference

AmountLamports
1 SOL1,000,000,000
0.012 SOL (reserve)12,000,000
0.002 SOL (Jito tip)2,000,000
0.0002 SOL (min tip)200,000