Skip to main content

How to Swap Tokens With the Titan Meta-Aggregation API on Solana

Updated on
Jun 30, 2026

13 min read

Overview

Getting the best swap price on Solana means comparing routes across fragmented liquidity, where each aggregator returns a single route from its own black-box router. Doing that yourself means integrating several aggregators and comparing their quotes on every swap.

The Titan Meta-Aggregation API does it in one request. Titan is a meta-aggregator: it fans a swap out to multiple liquidity providers at once, simulates each route against live onchain state, and returns every competing quote plus the expected winner. It is available through your Quicknode Solana endpoint, so there is no separate integration to maintain.

By the end of this guide, you will have a working Solana swap UI powered by Titan through Quicknode, where users pick tokens, watch providers compete for the best route, and execute a swap end to end.


TLDR
  • Build a Solana swap UI using the Titan Meta-Aggregation API served through a Quicknode endpoint
  • Fetch a lightweight indicative price with /quote/price for live display, then run the full provider race with /quote/swap
  • Pick the winning route with metadata.ExpectedWinner and display every competing provider quote
  • Build a VersionedTransaction yourself from Titan's composable instructions and address lookup tables
  • Sign with the connected wallet and submit through Quicknode RPC

What You Will Do


  • Clone and run the companion sample app
  • Fetch an indicative price for unconnected users and the full provider race for connected users
  • Build a VersionedTransaction from Titan's instructions and address lookup tables
  • Sign, send, and confirm the swap through Quicknode RPC

What You Will Need

This guide assumes you have a basic understanding of building TypeScript dApps, DeFi, and working with Solana wallets.

If you need a refresher, refer to:


You will also need:


  • A small amount of mainnet-beta SOL for fees
  • A small amount of tokens on mainnet to swap (USDC is recommended)
  • A Quicknode Solana mainnet endpoint. Get yours by signing up with Quicknode
  • The Titan Meta-Aggregation API enabled on your endpoint

danger

The Titan Meta-Aggregation API is only available on Solana mainnet-beta. That means if you follow this guide and execute swaps, you will be trading real tokens and paying real network fees, and you could lose value due to price movement, slippage, or selecting the wrong token pair.

How Does the Titan Meta-Aggregation API Work?

The Titan Meta-Aggregation API is a Solana swap API that fans one request out to multiple liquidity providers at once, including Titan's own router, third-party DEX aggregators, and request-for-quote (RFQ) venues, then simulates each returned route against live onchain state and returns every competing quote alongside the one expected to win.

The practical difference from a normal aggregator is what comes back. A single-engine aggregator returns one route for a pair; Titan returns each provider's independent route, so your app sees the full competition to display or pick from rather than one black-box answer.

Three properties make this practical to integrate:


  • Competing providers. A single /quote/swap call returns a quotes map keyed by provider. Each entry is a complete, executable route for that provider. The metadata.ExpectedWinner field names the provider Titan expects to give the best execution, so you can pick a winner automatically or present the full race to the user.
  • Simulation-verified routes. Before a route is returned, Titan simulates it against fresh onchain state. The output amount you display closely matches what actually executes, which reduces failed transactions caused by stale pool data.
  • Composable instructions. Titan does not hand back a sealed, ready-to-send transaction. Each route includes the raw swap instructions and the address lookup tables (ALTs) they reference. You assemble, sign, and send the transaction yourself, which leaves room to wrap the swap in your own logic.

Here is the lifecycle your swap UI follows in this guide:

Preview Price Quote

Before a wallet is connected, you only need an indicative rate to display as the user types. The GET /api/v1/quote/price endpoint is lightweight: it returns expected output and price impact without building any instructions or running the full provider race. This is the right call for live price updates.

Run the Provider Race

Once a wallet is connected and the user is ready to act, you call GET /api/v1/quote/swap with their userPublicKey. This runs the full meta-aggregation: every active provider competes, and the response includes each provider's executable instructions, ALTs, route steps, and the expected winner. The sample app sorts the quotes best-output-first and keeps the expected winner on top.

Send the Transaction

Because Titan returns instructions rather than a finished transaction, you resolve the route's ALTs from RPC, compile a v0 VersionedTransaction, have the wallet sign it, and submit it through Quicknode RPC. You own the transaction lifecycle end to end.

Discover Providers and Venues

Titan also exposes metadata endpoints that describe the breadth of the aggregation. GET /api/v1/providers lists the providers currently competing, and GET /api/v1/venues lists the routable onchain venues (with their program IDs). These rarely change within a session and power the "provider race" and "venues touched" displays in the sample UI.

Run Sample Application

To get started, clone the sample application repository and open the solana/titan-swap folder.

git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/solana/titan-swap
npm install
npm run dev

The app reads two server-only environment variables from .env.local, and both come from the same Quicknode Solana endpoint. QUICKNODE_RPC_URL is your endpoint URL itself, which you can copy from the Endpoints page of the Quicknode dashboard. TITAN_GATEWAY_URL is that same URL with /addon/1147/ appended (1147 is the add-on ID for the Titan Meta-Aggregation API), which becomes available once you enable the add-on on that endpoint.

.env.local
# Quicknode Solana RPC endpoint (server-only, never exposed to the browser).
QUICKNODE_RPC_URL=https://your-endpoint.solana-mainnet.quiknode.pro/YOUR_TOKEN/

# Titan Meta-Aggregation API base URL: your RPC endpoint above with
# /addon/1147/ appended. The proxy adds the trailing /api/v1 for you.
TITAN_GATEWAY_URL=https://your-endpoint.solana-mainnet.quiknode.pro/YOUR_TOKEN/addon/1147/

Once you have it running locally, take a quick look through the codebase to familiarize yourself with the structure, then come back to this guide to walk through each integration step in detail.

Sample Swap UI

This sample application is a swap UI wired around a price preview, a multi-provider swap race, and a build → sign → send → confirm flow. The UI components and state are already in place; the sections below walk through the modules that talk to Titan and assemble the transaction.

Call Titan API Server-Side

Every Titan request is made from a server-only module so your API credentials never reach the browser. The API encodes responses as MessagePack binary rather than JSON, so the client requests application/vnd.msgpack, attaches the bearer token when one is configured, and decodes the response before handing clean data back. This single helper is the entry point for every Titan endpoint used in the app.

lib/titan-server.ts
import "server-only";
import { decode } from "@msgpack/msgpack";

// TITAN_GATEWAY_URL — your Titan Meta-Aggregation API base URL (with or without trailing /api/v1)
// TITAN_GATEWAY_AUTH — optional bearer token (if not already in the URL)
const RAW_BASE = process.env.TITAN_GATEWAY_URL;
const AUTH = process.env.TITAN_GATEWAY_AUTH;

function baseUrl(): string {
if (!RAW_BASE) {
throw new Error("TITAN_GATEWAY_URL is not set. Add your API URL to .env.local.");
}
let b = RAW_BASE.replace(/\/+$/, "");
if (!b.endsWith("/api/v1")) b = `${b}/api/v1`;
return b;
}

type QueryValue = string | number | boolean | undefined;

async function titanGet<T = unknown>(
path: string,
params?: Record<string, QueryValue>
): Promise<T> {
const url = new URL(`${baseUrl()}${path}`);
if (params) {
for (const [k, v] of Object.entries(params)) {
if (v !== undefined && v !== "") url.searchParams.set(k, String(v));
}
}

const headers: Record<string, string> = { Accept: "application/vnd.msgpack" };
if (AUTH) headers.Authorization = `Bearer ${AUTH}`;

const res = await fetch(url.toString(), { headers, cache: "no-store" });
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Titan ${path} failed: ${res.status} ${text.slice(0, 300)}`);
}

const buf = new Uint8Array(await res.arrayBuffer());
return decode(buf) as T;
}

The thin Next.js route handlers under /api/titan/*, the /api/rpc proxy that forwards Solana JSON-RPC to your Quicknode endpoint, and the client-side fetch wrappers are standard framework plumbing and are omitted here.

Load the Token List

The Titan Meta-Aggregation API is a swap aggregator, not a token directory, so it does not ship a token list. The sample app pulls verified token metadata (symbol, name, decimals, logo) from a public registry purely for display and decimals, with a short list of common tokens (SOL, USDC, USDT) as a fallback. No routing or pricing comes from this list; that all flows through Titan. Because none of this touches the Titan API, the token-list loader is left as standard app code in the sample repo.

Fetch Wallet Balances

When a wallet connects, the app fetches its SOL and SPL token balances through Quicknode RPC. These balances populate the "From" selector (richest holdings first), validate that the user has enough to swap, and drive the "Max" button. This is plain RPC plumbing, not a Titan call, so the balances hook is left as standard app code in the sample repo.

Fetch Price Quote

When no wallet is connected, the app calls /quote/price for the indicative rate described above, debounced as the user types so the live display stays responsive without exhausting the rate limit.

lib/titan-server.ts
export async function fetchPrice(params: {
inputMint: string;
outputMint: string;
amount: string;
slippageBps?: number;
}): Promise<TitanPriceResponse> {
const raw = await titanGet<any>("/quote/price", {
inputMint: params.inputMint,
outputMint: params.outputMint,
amount: params.amount,
slippageBps: params.slippageBps,
});
return {
inputAmount: String(raw?.inputAmount ?? params.amount),
outputAmount: String(raw?.outputAmount ?? raw?.outAmount ?? "0"),
priceImpact: raw?.priceImpact != null ? Number(raw.priceImpact) : undefined,
};
}

Fetch Swap Quote

When a wallet is connected, the app calls /quote/swap with the user's public key and the simulate flag. This is where the meta-aggregation happens: the response contains a quotes map keyed by provider plus a metadata.ExpectedWinner field. The app requests quotes from every provider (the Gateway caps this at 10), reads the winner, normalizes each route, drops providers that failed to produce a usable route, and sorts best-output-first while keeping the expected winner on top.

lib/titan-server.ts
export async function fetchSwap(params: {
inputMint: string;
outputMint: string;
amount: string;
userPublicKey: string;
slippageBps?: number;
simulate?: boolean;
}): Promise<TitanSwapResponse> {
const raw = await titanGet<any>("/quote/swap", {
inputMint: params.inputMint,
outputMint: params.outputMint,
amount: params.amount,
userPublicKey: params.userPublicKey,
slippageBps: params.slippageBps,
simulate: params.simulate,
// Ask every provider to quote so the race is populated (server max is 10).
numQuotes: 10,
});

const quotesMap = raw?.quotes ?? {};
const expectedWinner: string | null = raw?.metadata?.ExpectedWinner ?? null;

const quotes: TitanSwapRoute[] = Object.entries(quotesMap)
.map(([provider, route]) => normRoute(provider, route))
// Drop providers that failed to produce a usable route.
.filter((q) => Number(q.outAmount) > 0);

// Sort best output first; keep the expected winner on top if present.
quotes.sort((a, b) => {
if (expectedWinner) {
if (a.provider === expectedWinner) return -1;
if (b.provider === expectedWinner) return 1;
}
return Number(b.outAmount) - Number(a.outAmount);
});

return { quotes, expectedWinner };
}

The simulate parameter maps to the UI's Accurate vs. Fast toggle. Simulating each route against live onchain state is more accurate but adds latency; turning it off returns faster, less-verified quotes. The UI then renders the competing quotes side by side, shows how many basis points each trails the leader, marks the expected winner, and passes the winning route to the swap step.

Each route in the response arrives as MessagePack with compact keys, and pubkeys come through as raw byte buffers, so a normalization pass converts everything into the base58 / base64 shape the transaction builder expects.

lib/titan-server.ts
function toBase58(v: unknown): string {
if (typeof v === "string") return v;
if (v instanceof Uint8Array) return bs58.encode(v);
if (Array.isArray(v)) return bs58.encode(Uint8Array.from(v as number[]));
return String(v ?? "");
}

function toBase64(v: unknown): string {
if (typeof v === "string") return v;
if (v instanceof Uint8Array) return Buffer.from(v).toString("base64");
if (Array.isArray(v)) return Buffer.from(Uint8Array.from(v as number[])).toString("base64");
return "";
}

function normStep(s: any) {
return {
label: String(s.label ?? s.amm ?? "Unknown"),
ammKey: toBase58(s.ammKey),
inputMint: toBase58(s.inputMint),
outputMint: toBase58(s.outputMint),
inAmount: String(s.inAmount ?? ""),
outAmount: String(s.outAmount ?? ""),
// allocPpb is parts-per-billion; convert to a percentage.
allocPct: s.allocPpb != null ? Number(s.allocPpb) / 1e7 : 0,
};
}

// The API uses compact keys (p/a/d, p/s/w) but we read long forms too so a
// schema tweak upstream doesn't silently break the builder.
function normInstruction(ix: any): TitanInstruction {
return {
programId: toBase58(ix.p ?? ix.programId),
accounts: (ix.a ?? ix.accounts ?? []).map((a: any) => ({
pubkey: toBase58(a.p ?? a.pubkey),
isSigner: Boolean(a.s ?? a.isSigner),
isWritable: Boolean(a.w ?? a.isWritable),
})),
data: toBase64(ix.d ?? ix.data),
};
}

function normRoute(provider: string, r: any): TitanSwapRoute {
return {
provider,
inputAmount: String(r.inputAmount ?? r.inAmount ?? ""),
outAmount: String(r.outAmount ?? r.outputAmount ?? ""),
slippageBps: Number(r.slippageBps ?? 0),
priceImpact: r.priceImpact != null ? Number(r.priceImpact) : undefined,
computeUnitsSafe:
r.computeUnitsSafe != null ? Number(r.computeUnitsSafe) : undefined,
steps: (r.steps ?? []).map(normStep),
instructions: (r.instructions ?? []).map(normInstruction),
addressLookupTables: (r.addressLookupTables ?? []).map(toBase58),
expiresAtMs: r.expiresAtMs != null ? Number(r.expiresAtMs) : undefined,
expiresAfterSlot:
r.expiresAfterSlot != null ? Number(r.expiresAfterSlot) : undefined,
};
}

Build VersionedTransaction

This is the main integration difference from execute-for-you aggregators. Titan returns instructions and the ALTs they reference, so you build the transaction yourself. The builder resolves each ALT from Quicknode RPC, rebuilds each instruction from the normalized base58 program ID and base64 data, fetches a recent blockhash, and compiles a v0 message that passes the lookup tables in. This step is standard Solana transaction assembly rather than a Titan API call, so the full builder (lib/build-swap-tx.ts, built with @solana/kit) is left in the sample repo for you to read.


note

The address lookup tables in addressLookupTables must be fetched from RPC and passed when compiling the message. Skipping this step causes transaction compilation to fail, because the compacted account references in the route's instructions cannot be resolved without their tables.

Sign and Send Transaction

With the transaction built, the swap hook prompts the connected wallet to sign it, submits the raw transaction through Quicknode RPC, and confirms it. Because the RPC proxy carries only HTTP JSON-RPC (no WebSocket), the app confirms by polling getSignatureStatuses until the transaction is confirmed, errors, or its blockhash expires. The status flows through building → signing → sending → confirming → success, and the UI surfaces the signature with a link to verify the swap onchain.

hooks/useSwap.ts
const executeSwap = async (route: TitanSwapRoute, toToken: Token) => {
if (!signer) throw new Error("Wallet not connected");

const rpc = createRpc();

// Build the transaction from Titan's instructions + lookup tables.
setStatus("building");
const { message, lastValidBlockHeight } = await buildSwapTransaction(
route,
signer,
rpc
);

// Sign in the wallet via the Kit signer attached to the message.
setStatus("signing");
const signedTransaction = await signTransactionMessageWithSigners(message);

// Submit through Quicknode RPC.
setStatus("sending");
const signature = getSignatureFromTransaction(signedTransaction);
await rpc
.sendTransaction(getBase64EncodedWireTransaction(signedTransaction), {
encoding: "base64",
skipPreflight: false,
preflightCommitment: "confirmed",
})
.send();
setTxSignature(signature);

// Confirm by polling over HTTP (the RPC proxy carries no WebSocket).
setStatus("confirming");
await confirmBySignature(rpc, signature, lastValidBlockHeight);

setStatus("success");
};

Signing, sending, and confirming are standard Solana transaction plumbing rather than Titan calls, so the surrounding hook state and the confirmBySignature poller are left as standard app code in the sample repo.

With the transaction signed, sent, and confirmed, the swap is complete. This completes the full Titan flow: price preview → swap race → build → sign → send.

Wrapping Up

You have built a swap UI that turns token discovery, multi-provider routing, slippage handling, and transaction management into a single experience powered by the Titan Meta-Aggregation API. Because Titan returns competing quotes and composable instructions rather than a sealed transaction, you saw exactly which providers were bidding, which route won, and how to assemble and send the transaction yourself. That ownership of the transaction lifecycle gives you a solid foundation to extend, whether that means adding priority fees, custom instructions, or your own landing strategy.

Frequently Asked Questions

What is the Titan Meta-Aggregation API?

The Titan Meta-Aggregation API is a Solana swap API that sources executable quotes from multiple liquidity providers in a single request. Rather than routing through one engine, it fans a request out to several providers, simulates each route against live onchain state, and returns every competing quote along with the one it expects to win.

How does meta-aggregation differ from a regular DEX aggregator?

A regular aggregator returns its own single best route. A meta-aggregator queries multiple providers at once, including its own router, other aggregators, and RFQ venues, and returns all of their routes so your app can compare them or pick the expected winner. You get the full competition instead of one black-box answer.

Why do I have to build the transaction myself?

Titan returns composable instructions and the address lookup tables they reference instead of a sealed transaction. You resolve the lookup tables from RPC, compile a v0 VersionedTransaction, sign it with the connected wallet, and send it. This gives you full control to wrap the swap in custom logic, but it means you must load the lookup tables before compiling or the transaction will fail to build.

When should I use the price endpoint versus the swap endpoint?

Use /quote/price for lightweight live display while the user types, since it returns expected output without building instructions or running the full provider race. Call /quote/swap only when a wallet is connected and the user is ready to act, since it runs the full meta-aggregation and returns executable instructions and address lookup tables.

Is the Titan Meta-Aggregation API available on Solana testnet or only mainnet?

It is only available on Solana mainnet-beta. Any swaps you execute will trade real tokens and incur real network fees, with potential for value loss due to price movement, slippage, or selecting the wrong token pair.

What do I need to integrate the Titan Meta-Aggregation API?

You need a Quicknode Solana mainnet endpoint with the Titan Meta-Aggregation API enabled, basic TypeScript and Solana dApp experience, and small amounts of mainnet SOL and tokens for testing. Both the Quicknode RPC token and the API credentials should stay server-side behind proxy routes.

Resources


We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide