> For the complete documentation index, see [llms.txt](https://titan-exchange.gitbook.io/titan/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://titan-exchange.gitbook.io/titan/developer-doc/swap-api/guides/stream-and-execute.md).

# 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](/titan/developer-doc/swap-api/quickstart.md).

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`](/titan/developer-doc/resources/sdk.md) SDK.

{% hint style="info" %}
You need an API token and endpoint URL before starting. See [Get API Access](/titan/developer-doc/getting-started/api-access.md) if you don't have one yet.
{% endhint %}

## Prerequisites

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

Set your environment variables:

```bash
export TITAN_ENDPOINT="wss://YOUR_ENDPOINT/api/v1/ws"
export TITAN_API_KEY="YOUR_API_TOKEN"
export SOLANA_RPC_URL="https://YOUR_RPC_ENDPOINT"
```

***

{% stepper %}
{% step %}

#### Connect and negotiate the protocol

Titan Direct uses [MessagePack](/titan/developer-doc/swap-api/reference/wire-protocol.md) over WebSocket. List your supported compression schemes in the [`Sec-WebSocket-Protocol`](/titan/developer-doc/swap-api/reference/direct/connection.md) header — the server selects the best match and confirms it on open.

```typescript
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_API_KEY}`;

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

{% endstep %}

{% step %}

#### Call GetInfo

Send [`GetInfo`](/titan/developer-doc/swap-api/reference/direct/get-info.md) right after connecting to confirm the connection and read the server's current defaults — update interval, slippage bounds, and stream limits.

```typescript
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');
  }
});
```

{% endstep %}

{% step %}

#### Open a quote stream

Send [`NewSwapQuoteStream`](/titan/developer-doc/swap-api/reference/direct/new-swap-quote-stream.md) to start receiving live quotes. The `swap` object defines what to quote, `transaction` provides the wallet context needed to build executable instructions.

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

{% hint style="warning" %}
Use `BigInt` for `amount`. Numbers above 2^32 encode as float64 in MessagePack, which the server rejects.
{% endhint %}
{% endstep %}

{% step %}

#### Read quotes and pick the best

The server pushes [`StreamData`](/titan/developer-doc/swap-api/reference/types.md) messages at the negotiated interval. Each update includes `metadata.ExpectedWinner` — **Titan's recommendation for the best slippage-adjusted route.**

```typescript
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`);
    }
  }
});
```

{% endstep %}

{% step %}

#### Build, sign, and send

Each quote returns `instructions` and `addressLookupTables` as part of the [`SwapRoute`](/titan/developer-doc/swap-api/reference/types.md) 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.

```typescript
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);
  }
}
```

{% hint style="warning" %}
If a route expires, `expiresAtMs` contains the expiry as a millisecond UNIX timestamp and `expiresAfterSlot` contains the last slot at which the route is valid. If present, check these before executing — a route may no longer be valid by the time your transaction lands on-chain.
{% endhint %}
{% endstep %}

{% step %}

#### Stop the stream

Once you've executed, send [`StopStream`](/titan/developer-doc/swap-api/reference/direct/stop-stream.md) to free up the connection for other streams.

```typescript
sendRequest({ StopStream: { id: streamId! } });
```

{% endstep %}

{% step %}

#### Handle StreamEnd and errors

```typescript
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](/titan/developer-doc/swap-api/guides/error-handling.md).
{% endstep %}
{% endstepper %}

***

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

***

## Related pages

* [Configure Routing](/titan/developer-doc/swap-api/guides/configure-routing.md) — filter venues and providers
* [Error Handling & Reconnect](/titan/developer-doc/swap-api/guides/error-handling.md) — error codes and reconnect patterns
* [Wire Protocol](/titan/developer-doc/swap-api/reference/wire-protocol.md) — MessagePack encoding and compression
* [Connection & Negotiation](/titan/developer-doc/swap-api/reference/direct/connection.md) — protocol negotiation details
* [Types Reference](/titan/developer-doc/swap-api/reference/types.md) — `SwapRoute`, `StreamData`, and all type definitions
* [NewSwapQuoteStream Reference](/titan/developer-doc/swap-api/reference/direct/new-swap-quote-stream.md)
* [CPI Example (Anchor)](https://github.com/Titan-Pathfinder/titan-v2-cpi-example) — Example Anchor program demonstrating cross-program invocation into Titan's `swap_route_v2` instruction
