For the complete documentation index, see llms.txt. This page is also available as Markdown.

Stream & Execute a Swap

Connect to Titan Direct, stream live quotes, and execute a swap end to end.

This guide covers Titan Direct — the WebSocket path. For a single-request flow using Titan Gateway, see the Quickstart; for self-serve REST with an API key, see the Developers Portal Quickstart.

It walks through the complete lifecycle — connecting over WebSocket, streaming live quotes, picking the best one, building and signing a transaction, then shutting down cleanly. This guide uses raw WebSocket and MessagePack directly. If you prefer a higher-level interface, see the @titanexchange/sdk-ts SDK.

You need a distributor JWT and endpoint URL before starting. See Get API Access if you don't have one yet.

Prerequisites

npm install ws @msgpack/msgpack bs58 @solana/web3.js http-encoding

Set your environment variables:

export TITAN_ENDPOINT="wss://YOUR_ENDPOINT/api/v1/ws"
export TITAN_JWT="YOUR_JWT"
export SOLANA_RPC_URL="https://YOUR_RPC_ENDPOINT"

1

Connect and negotiate the protocol

Titan Direct uses MessagePack over WebSocket. List your supported compression schemes in the Sec-WebSocket-Protocol header — the server selects the best match and confirms it on open.

import WebSocket from 'ws';
import { Encoder, Decoder } from '@msgpack/msgpack';
import bs58 from 'bs58';
import {
  zstdCompress, zstdDecompress,
  brotliCompress, brotliDecompress,
  gzipCompress, gzipDecompress,
} from 'http-encoding';

// useBigInt64 ensures amounts encode as int64, not float64
const encoder = new Encoder({ useBigInt64: true });
const decoder = new Decoder({ useBigInt64: true });

// Build the WebSocket URL with auth token as query param
const url = `${process.env.TITAN_ENDPOINT}?auth=${process.env.TITAN_JWT}`;

// List supported protocols in preference order — server picks the best match
// zstd gives the best compression ratio, fallback to brotli, gzip, or none
const ws = new WebSocket(url, [
  'v1.api.titan.ag+zstd',    // preferred — best ratio + speed
  'v1.api.titan.ag+brotli',  // fallback
  'v1.api.titan.ag+gzip',    // fallback
  'v1.api.titan.ag',          // no compression
]);

// Compress/decompress default to identity (no-op) — overwritten on open
let compress: (data: Uint8Array) => Promise<Uint8Array> | Uint8Array = (d) => d;
let decompress: (data: Uint8Array) => Promise<Uint8Array> | Uint8Array = (d) => d;
let requestId = 0;

ws.on('open', () => {
  // The server confirms which protocol it selected via ws.protocol
  const proto = ws.protocol;
  console.log('Connected — protocol:', proto);

  // Match the negotiated protocol to the correct codec
  if (proto.endsWith('+zstd')) {
    compress = zstdCompress;
    decompress = zstdDecompress;
  } else if (proto.endsWith('+brotli')) {
    compress = brotliCompress;
    decompress = brotliDecompress;
  } else if (proto.endsWith('+gzip')) {
    compress = gzipCompress;
    decompress = gzipDecompress;
  }
  // If none matched, no compression — identity functions stay in place
});

// Encode a request as MessagePack, compress, and send
async function sendRequest(data: Record<string, unknown>): Promise<number> {
  const id = requestId++;
  const encoded = encoder.encode({ id, data });
  ws.send(await compress(encoded));
  return id;
}

// Decompress an incoming binary frame and decode from MessagePack
async function decodeMessage(raw: Buffer): Promise<any> {
  const data = await decompress(raw);
  return decoder.decode(data);
}

ws.on('error', (err) => {
  console.error('WebSocket error:', err.message);
});
2

Call GetInfo

Send GetInfo right after connecting to confirm the connection and read the server's current defaults — update interval, slippage bounds, and stream limits.

ws.on('open', () => {
  sendRequest({ GetInfo: {} });
});

ws.on('message', async (raw: Buffer) => {
  const msg = await decodeMessage(raw);

  if ('Response' in msg && 'GetInfo' in msg.Response.data) {
    const info = msg.Response.data.GetInfo;
    console.log('Protocol version:', info.protocolVersion);
    console.log('Default update interval:', info.settings.quoteUpdate.intervalMs.default, 'ms');
  }
});
3

Open a quote stream

Send NewSwapQuoteStream to start receiving live quotes. The swap object defines what to quote, transaction provides the wallet context needed to build executable instructions.

const SOL  = bs58.decode('So11111111111111111111111111111111111111112');
const USDC = bs58.decode('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const userPublicKey = bs58.decode('YOUR_WALLET_PUBLIC_KEY');

sendRequest({
  NewSwapQuoteStream: {
    swap: {
      inputMint: SOL,
      outputMint: USDC,
      amount: 1_000_000_000n, // 1 SOL in lamports
      slippageBps: 50,
    },
    transaction: {
      userPublicKey,
    },
  },
});

The server responds with a stream.id — save it to stop the stream later.

4

Read quotes and pick the best

The server pushes StreamData messages at the negotiated interval. Each update includes metadata.ExpectedWinner — Titan's recommendation for the best slippage-adjusted route.

let streamId: number | undefined;

ws.on('message', async (raw: Buffer) => {
  const msg = await decodeMessage(raw);

  if ('Response' in msg && 'NewSwapQuoteStream' in msg.Response.data) {
    streamId = msg.Response.stream.id;
    console.log(`Stream ${streamId} open`);
    return;
  }

  if ('StreamData' in msg) {
    const swapQuotes = msg.StreamData.payload.SwapQuotes;

    // Use metadata.ExpectedWinner for the best slippage-adjusted route
    const winner = swapQuotes.metadata?.ExpectedWinner;
    const bestRoute = winner && swapQuotes.quotes[winner];

    if (bestRoute?.instructions?.length) {
      console.log(`Best: ${winner} — ${bestRoute.outAmount} out`);
    }
  }
});
5

Build, sign, and send

Each quote returns instructions and addressLookupTables as part of the SwapRoute type. Fetch the ALT accounts, compile a V0 message, sign, and send.

Use computeUnitsSafe for your compute budget — it accounts for on-chain variance and gives your transaction room to land without failing on a slightly heavier slot.

import {
  Connection,
  VersionedTransaction,
  TransactionMessage,
  AddressLookupTableAccount,
  TransactionInstruction,
  PublicKey,
} from '@solana/web3.js';

const connection = new Connection(process.env.SOLANA_RPC_URL!);

function toSolanaInstruction(ix: any): TransactionInstruction {
  return new TransactionInstruction({
    programId: new PublicKey(ix.p),
    keys: ix.a.map((acc: any) => ({
      pubkey: new PublicKey(acc.p),
      isSigner: acc.s,
      isWritable: acc.w,
    })),
    data: Buffer.from(ix.d),
  });
}

async function executeQuote(route: any): Promise<void> {
  const altAccounts: AddressLookupTableAccount[] = [];
  for (const altPubkey of (route.addressLookupTables ?? [])) {
    const { value } = await connection.getAddressLookupTable(new PublicKey(altPubkey));
    if (value) altAccounts.push(value);
  }

  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();

  const message = new TransactionMessage({
    payerKey: new PublicKey(userPublicKey),
    recentBlockhash: blockhash,
    instructions: route.instructions.map(toSolanaInstruction),
  }).compileToV0Message(altAccounts);

  const tx = new VersionedTransaction(message);

  // Sign the transaction — use your wallet adapter or Keypair
  // Client-side:  const signed = await signTransaction(tx);
  // Server-side:  tx.sign([keypair]);

  try {
    const sig = await connection.sendRawTransaction(tx.serialize());
    await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight });
    console.log('Confirmed:', sig);

    if (streamId !== undefined) {
      sendRequest({ StopStream: { id: streamId } });
    }
  } catch (err: any) {
    console.error('Send failed:', err.message);
  }
}
6

Stop the stream

Once you've executed, send StopStream to free up the connection for other streams.

sendRequest({ StopStream: { id: streamId! } });
7

Handle StreamEnd and errors

ws.on('message', async (raw: Buffer) => {
  const msg = await decodeMessage(raw);

  if ('Error' in msg) {
    console.error(`Request ${msg.Error.requestId} failed [${msg.Error.code}]: ${msg.Error.message}`);
  }

  if ('StreamEnd' in msg) {
    const { id, errorCode, errorMessage } = msg.StreamEnd;
    if (errorCode) {
      console.error(`Stream ${id} error ${errorCode}: ${errorMessage}`);
    } else {
      console.log(`Stream ${id} closed`);
    }
    ws.close();
  }
});

For reconnect patterns see Error Handling & Reconnect.


Key details

  • quotes shape — Record<string, SwapRoute> keyed by provider ID, not an array.

  • Transaction building — Build a V0 transaction from instructions + addressLookupTables.

  • Compute budget — Use computeUnitsSafe — accounts for on-chain variance.

  • Quote expiry — If a route expires, expiresAtMs and expiresAfterSlot will be set. Check these before executing.


Last updated