Skip to main content
This guide walks through a complete private trade end-to-end: environment setup, depositing funds, building a swap transaction via your DEX aggregator, executing privately via Vanish, and committing the result.
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

1. Environment Setup

Before starting, make sure you have the following available:
  • Vanish API key - provisioned during onboarding
  • Solana RPC endpoint - any standard Solana RPC URL
  • User keypair - the Ed25519 keypair for the wallet you’re trading from
  • Solana balance - enough SOL to cover the deposit and transaction fees
Store sensitive values - your API key and keypair - in environment variables. Never expose them in source code or client-side bundles.

2. Set Up the Client

Set up your Vanish HTTP client and Solana connection. These are used throughout the rest of the guide.
import { Connection, Keypair } from '@solana/web3.js';

const VANISH_URL  = 'https://core-api.vanish.trade';
const connection  = new Connection(process.env.SOLANA_RPC_URL!);
const userKeypair = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(process.env.SOLANA_KEYPAIR!))
);

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();
}

3. Sign Requests

Several endpoints require a signed message proving ownership of user_address. Sign with the user’s Ed25519 keypair and base64-encode the result. The message format varies by endpoint - see Signing Requests for all formats.
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');
}

// timestamp must be Date.now().toString() — milliseconds, not seconds
function readSignature(timestamp: string, keypair: Keypair): string {
  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
  );
}

// all args must match exactly what you pass to /trade/create
function tradeSignature(
  source: string, target: string, amount: string,
  loanSol: string, timestamp: string, jitoTip: string,
  keypair: Keypair
): string {
  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
  );
}

4. Fund Your Account

Before trading, you need a Vanish balance. This involves three steps: fetching a deposit address, sending funds on-chain, then committing the transaction to Vanish.
1

Get a deposit address

// 11111111111111111111111111111111 is the SOL native mint
// replace with the token's mint address for SPL deposits
const { address: depositAddress } = await vanish(
  `/deposit_address?token_address=11111111111111111111111111111111`
);
console.log('Deposit to:', depositAddress);
See GET /deposit_address for full reference. Always fetch a fresh address before each deposit - addresses may rotate to ensure maximum privacy.
2

Send SOL on-chain

Send funds to the deposit address and wait for confirmation.
import { PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';

const tx = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: userKeypair.publicKey,
    toPubkey:   new PublicKey(depositAddress),
    lamports:   100_000_000, // 0.1 SOL
  })
);

const depositTxId = await sendAndConfirmTransaction(connection, tx, [userKeypair]);
console.log('Deposit tx:', depositTxId);
3

Commit the deposit to Vanish

Once confirmed on-chain, notify Vanish by calling POST /commit with the transaction signature.
const commit = await vanish('/commit', {
  method: 'POST',
  body: JSON.stringify({ tx_id: depositTxId }),
});

console.log('Deposit status:', commit.status);
// completed  = balance updated, ready to trade
// pending    = compliance screening in progress, check back shortly
// rejected   = failed screening, funds will be refunded automatically
If the status is pending, poll /commit again with the same tx_id until it resolves to completed.

5. Build a Swap Transaction

Vanish wraps your swap instructions - it does not build the route itself. Fetch an unsigned transaction from your DEX aggregator of choice and pass it to Vanish.
Critical: When building the swap transaction, set the one-time wallet as the transaction signer - not the user’s wallet. Pass the one-time wallet address wherever your aggregator asks for the signing wallet or user public key.
1

Get a one-time wallet from Vanish

const { address: oneTimeWallet } = await vanish('/trade/one-time-wallet');
See GET /trade/one-time-wallet for full reference. Never reuse this address - fetch a fresh one for every trade.
2

Fetch a quote (Jupiter example)

const SOL_MINT    = '11111111111111111111111111111111';
const TARGET_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // USDC
const AMOUNT      = '100000000'; // 0.1 SOL in lamports

const quoteRes = await fetch(
  `https://quote-api.jup.ag/v6/quote` +
  `?inputMint=${SOL_MINT}` +
  `&outputMint=${TARGET_MINT}` +
  `&amount=${AMOUNT}` +
  `&slippageBps=50`  // 0.5% slippage tolerance
);
const quote = await quoteRes.json();
3

Get the unsigned swap transaction (Jupiter example)

Pass the one-time wallet as the signer so the transaction is built around it, not the user’s wallet.
const swapRes = await fetch('https://quote-api.jup.ag/v6/swap', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    quoteResponse:    quote,
    userPublicKey:    oneTimeWallet,   // must be the one-time wallet
    wrapAndUnwrapSol: true,
    dynamicComputeUnitLimit: true,
  }),
});

const { swapTransaction } = await swapRes.json();
// swapTransaction is base64-encoded and ready for Vanish

6. Execute the Trade

Submit the swap transaction to Vanish via POST /trade/create. Vanish wraps and submits it via Jito, returning a tx_id. This step uses oneTimeWallet, swapTransaction, TARGET_MINT, and AMOUNT from the previous step.
const SOL_MINT  = '11111111111111111111111111111111';
const LOAN_SOL  = '12000000'; // recommended loan_additional_sol — unused amount is refunded
const JITO_TIP  = '1000000';  // 0.001 SOL — minimum recommended tip

const timestamp     = Date.now().toString();
const userSignature = tradeSignature(
  SOL_MINT, TARGET_MINT, AMOUNT,
  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: TARGET_MINT,
    amount:               AMOUNT,
    swap_transaction:     swapTransaction,  // base64-encoded unsigned tx from your aggregator
    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 route via Jito (default)
  }),
});

console.log('Trade submitted. tx_id:', trade.tx_id);

7. Commit the Trade

Always call POST /commit after every trade - whether it succeeded, failed, or expired on-chain. This resolves the user’s balance from its pending state.
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);
/commit must be called for every transaction - success, failure, or expiry. Without it, the user’s balance remains in a pending state indefinitely.
StatusMeaning
completedTrade settled: balance updated
pendingOn-chain confirmation or compliance check in progress: poll again shortly
failedTransaction rejected on-chain: balance released
expiredNot confirmed in time: balance released

8. Non-Jito Variant (Self-Broadcast)

By default, Vanish submits via Jito bundle and returns only a tx_id. If you want to broadcast yourself - for custom retry logic or a low-latency RPC - add the prefer_non_jito object to the same /trade/create request from step 6.
Use split_repay: 1 on the non-Jito route. Higher values increase transaction size and risk exceeding Solana’s limit.
Add this to your /trade/create request body:
"prefer_non_jito": {
  "compute_unit_price": "71429",
  "compute_unit_limit": "1400000",
  "custom_tip_address": "optional — your tip wallet address",
  "custom_tip_amount":  "optional — tip amount in lamports"
}
Then broadcast the returned transaction yourself instead of using the tx_id directly:
import { VersionedTransaction } from '@solana/web3.js';

// trade.transaction is the fully signed tx returned by Vanish — broadcast it yourself
const txBytes = Buffer.from(trade.transaction, 'base64');
const tx      = VersionedTransaction.deserialize(txBytes);
const txId    = await connection.sendRawTransaction(tx.serialize(), {
  skipPreflight: false, // simulate on RPC before submitting — catches obvious failures early
  maxRetries:    3,
});
await connection.confirmTransaction(txId, 'confirmed');

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

9. Check Balances

See POST /account/balances for full reference.
const timestamp = Date.now().toString();
const signature = readSignature(timestamp, userKeypair);

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

// [{ token_address, balance, program_id }, ...]
// balance is in lamports for SOL, base units for SPL tokens
console.log(balances);

10. Recover Interrupted Flows

If your app crashes after submitting a trade but before calling /commit, the user’s balance is held in a pending state. Call POST /account/pending on startup to catch and resolve these.
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 }),
    });
    if (!commit.already_processed) {
      console.log(`Resolved ${action.action_type}:`, commit.status);
    }
  }
}

Lamport Reference

All SOL amounts in the Vanish API are in lamports. SPL token amounts use the token’s own decimal precision.
AmountLamports
1 SOL1,000,000,000
0.012 SOL (loan_additional_sol)12,000,000
0.001 SOL (minimum recommended Jito tip)1,000,000

Review Your Integration

Before going live, work through these pages to complete your integration. Each one is the full reference for its part of the API.
1

Integration Guide

The Integration Guide is the full reference for every flow - deposit, trade, and withdraw. Go through it to verify:
  • Every field in your /trade/create request is correct, including split_repay and loan_additional_sol
  • Your signing format matches the exact message string for each endpoint type (read, trade, withdraw)
  • You understand the interrupted flow recovery pattern using /account/pending
2

Error Handling

The Error Handling page covers what happens when things go wrong. Go through it to verify:
  • You handle all five /commit statuses (completed, pending, failed, expired, rejected) correctly
  • You’re polling /commit with the same tx_id when status is pending, not creating a new transaction
  • You call /account/pending on startup to catch any uncommitted transactions
3

FAQ

The FAQ covers the most common integration mistakes. Go through it to verify:
  • You’re fetching a fresh one-time wallet for every trade - never reusing one
  • Your swap transaction is unsigned before being passed to /trade/create
  • Your timestamps are in milliseconds, not seconds
  • Your signature is base64-encoded, not base58 or hex
  • You understand the same-wallet-in, same-wallet-out compliance rule for withdrawals