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.
Prerequisites
npm install ws @msgpack/msgpack bs58 @solana/web3.js http-encodingSet 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"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);
});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');
}
});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.
Use BigInt for amount. Numbers above 2^32 encode as float64 in MessagePack, which the server rejects.
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`);
}
}
});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);
}
}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.
Stop the stream
Once you've executed, send StopStream to free up the connection for other streams.
sendRequest({ StopStream: { id: streamId! } });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
quotesshape —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,
expiresAtMsandexpiresAfterSlotwill be set. Check these before executing.
Related pages
Configure Routing — filter venues and providers
Error Handling & Reconnect — error codes and reconnect patterns
Wire Protocol — MessagePack encoding and compression
Connection & Negotiation — protocol negotiation details
Types Reference —
SwapRoute,StreamData, and all type definitionsCPI Example (Anchor) — Example Anchor program demonstrating cross-program invocation into Titan's
swap_route_v2instruction
Last updated

