> 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/transaction-template.md).

# Transaction Template

Reserve room in the swap transaction for your own instructions and address lookup tables — so the route Titan returns still fits when you assemble the final transaction.

When you build a swap transaction, you usually want to add instructions of your own — a compute-budget setting, a memo, a custom fee transfer, an oracle update, an app-specific log. The problem: every byte you add eats into the transaction size limit — 1232 bytes for V0 or 4096 bytes for Transaction V1 — and the route Titan returned may no longer fit.

`transactionTemplate` solves this. You tell Titan upfront what extra instructions, ALTs, and account metas will share the final transaction with the swap. Titan then sizes the route so the **assembled** transaction fits inside Solana's limits when you splice everything together.

{% hint style="info" %}
**`transactionTemplate` affects route sizing only.** Titan measures the template when selecting a route but does not return its contents. Each route contains the swap instructions alone — you construct the final transaction yourself.
{% endhint %}

{% hint style="warning" %}
**`transactionTemplate` is incompatible with `accountsLimitTotal`, `accountsLimitWritable`, and `sizeConstraint`.** The template **is** the sizing constraint — passing both returns an error.
{% endhint %}

## When to use it

* You're prepending or appending instructions that aren't part of the swap (compute-budget settings, memos, app-specific logs, oracle pokes, custom fee transfers).
* You're using your own ALTs (rebate program, fee program, app-specific routing).
* You're hitting tx-size errors after combining the route with your own instructions.

For everything else, the default sizing (`accountsLimitTotal` / `accountsLimitWritable`) is enough.

## Pair with V3

**Use `titanSwapVersion: 3` whenever you use `transactionTemplate`.** V3 manages input and output token accounts internally, so the router does **not** insert ATA create/close instructions around the swap — the full residual byte budget goes to the route. With V2, the router still adds wSOL wrap/unwrap and ATA-creation instructions, which makes templates less predictable.

{% hint style="danger" %}
**`titanSwapVersion` is the integer `3`, not the string `"V3"`.** The server rejects strings with `Failed to deserialize query string: titanSwapVersion: invalid digit found in string`.
{% endhint %}

## The template

See [`TransactionTemplate`](/titan/developer-doc/swap-api/reference/direct/new-swap-quote-stream.md#transaction-template) for the full struct. Four fields:

* **`i`** — Instructions you'll place in the final transaction **before** the swap. Titan measures them for sizing but does not return them, so retain your own copy for assembly. Include any ATA creation/deletion for the input and output mints yourself if you need them — the template doesn't assume the router will add them. (V3 doesn't need them; V2 might.)
* **`a`** — ALTs the surrounding transaction already references. **Order matters** — Solana resolves ALTs greedily. Provide them in the order you'll use when compiling the message. Titan appends the ALTs required for the swap when sizing the route, but that combined list is not returned; the response provides the route's ALT addresses separately, and you compile the final message from both sets.
* **`m`** — Extra account metas that belong to the surrounding transaction but aren't reachable from any instruction in `i`. Rare — leave empty unless you need it.
* **`c`** — Transaction-level priority fee and compute-budget settings. Required for Transaction V1 and ignored for V0. Transaction V1 carries these values in the transaction envelope rather than in `ComputeBudgetProgram` instructions.

{% hint style="warning" %}
With `transactionFormat: 1`, `c` is required and `a` is ignored because Transaction V1 does not support address lookup tables.
{% endhint %}

### Wire format

The MessagePack wire format uses **single-letter field names** for space efficiency.

| Type                        | Wire field | Meaning                                                           |
| --------------------------- | ---------- | ----------------------------------------------------------------- |
| `TransactionTemplate`       | `i`        | `instructions` (array)                                            |
| `TransactionTemplate`       | `a`        | `alts` (array)                                                    |
| `TransactionTemplate`       | `m`        | `accountMetas` (array)                                            |
| `TransactionTemplate`       | `c`        | `TransactionConfigParams` (object; required for V1)               |
| `Instruction`               | `p`        | `programId` (32-byte pubkey)                                      |
| `Instruction`               | `a`        | `accounts` (array of `AccountMeta`)                               |
| `Instruction`               | `d`        | `data` (raw bytes)                                                |
| `AccountMeta`               | `p`        | `pubkey` (32-byte pubkey)                                         |
| `AccountMeta`               | `s`        | `isSigner` (bool)                                                 |
| `AccountMeta`               | `w`        | `isWritable` (bool)                                               |
| `AddressLookupTableAccount` | `p`        | `key` (32-byte ALT account address)                               |
| `AddressLookupTableAccount` | `a`        | `addresses` (array of 32-byte pubkeys *inside* the ALT, in order) |

**All pubkeys and instruction `data` are raw byte arrays, not Base58 or Base64 strings.**

## REST vs WebSocket transport

The template payload is the same shape on both transports, but the wrapping is different:

* **REST (Gateway):** MessagePack-encode the template, then **Base64-encode the bytes**, and pass as the `transactionTemplate` query-string value.
* **WebSocket (Direct):** Embed the template **inline** inside the `swap` object of your `NewSwapQuoteStream` request. The whole frame is MessagePack already — no Base64 wrapping.

## Example: V3 + compute-budget template

A USDC → SOL swap on V3 with a minimal template containing only the two compute-budget instructions. This is the canonical real-world pattern.

{% tabs %}
{% tab title="Titan Gateway (REST)" %}

```typescript
import { Encoder, decode } from '@msgpack/msgpack';
import bs58 from 'bs58';
import {
  Connection,
  MessageV0,
  PublicKey,
  TransactionInstruction,
  VersionedTransaction,
} from '@solana/web3.js';

const encoder = new Encoder({ useBigInt64: true });

// --- Step 1: build the two compute-budget instructions ---
const CB_PROGRAM_ID = new PublicKey('ComputeBudget111111111111111111111111111111');

function setComputeUnitLimitData(units: number): Uint8Array {
  // Discriminator 0x02 + u32 LE
  const data = new Uint8Array(5);
  data[0] = 0x02;
  new DataView(data.buffer).setUint32(1, units, true);
  return data;
}

function setComputeUnitPriceData(microLamports: bigint): Uint8Array {
  // Discriminator 0x03 + u64 LE
  const data = new Uint8Array(9);
  data[0] = 0x03;
  new DataView(data.buffer).setBigUint64(1, microLamports, true);
  return data;
}

// --- Step 2: assemble the TransactionTemplate using wire-format field names ---
const template = {
  i: [
    { p: CB_PROGRAM_ID.toBytes(), a: [], d: setComputeUnitLimitData(1_400_000) },
    { p: CB_PROGRAM_ID.toBytes(), a: [], d: setComputeUnitPriceData(0n) }, // sub real priority fee in prod
  ],
  a: [],
  m: [],
};

// --- Step 3: MessagePack-encode, then Base64-encode ---
const transactionTemplate = Buffer.from(encoder.encode(template)).toString('base64');

// --- Step 4: send the quote request ---
const USDC = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
const SOL  = 'So11111111111111111111111111111111111111112';
const USER = 'Affd7LDkUY9fjWjjQSr9bvises1Cku4WSwdLNGBhgVW3';

const params = new URLSearchParams({
  inputMint: USDC,
  outputMint: SOL,
  amount: '100000000',           // 100 USDC
  userPublicKey: USER,
  slippageBps: '50',
  titanSwapVersion: '3',         // integer 3, not "V3"
  simulate: 'false',
  transactionTemplate,
});

const res = await fetch(
  `${process.env.TITAN_ENDPOINT}/api/v1/quote/swap?${params}`,
  {
    headers: {
      'Authorization': `Bearer ${process.env.TITAN_JWT}`,
      'Accept': 'application/vnd.msgpack',
    },
  }
);

const quotes = decode(new Uint8Array(await res.arrayBuffer())) as any;
const winner = quotes.metadata?.ExpectedWinner;
const route = winner && quotes.quotes[winner];

// --- Step 5: assemble [templateInstructions, ...routeInstructions] ---
function titanIxToTransactionIx(ix: any): TransactionInstruction {
  return new TransactionInstruction({
    programId: new PublicKey(ix.p),
    data: Buffer.from(ix.d),
    keys: ix.a.map((a: any) => ({
      pubkey: new PublicKey(a.p),
      isSigner: a.s,
      isWritable: a.w,
    })),
  });
}

const templateIxs = template.i.map(titanIxToTransactionIx);
const swapIxs = route.instructions.map(titanIxToTransactionIx);
const allIxs = [...templateIxs, ...swapIxs];

// Compile into a v0 message with the route's ALTs, sign and send
```

{% endtab %}

{% tab title="Titan Direct (WebSocket)" %}
On the WebSocket transport, the whole frame is already MessagePack — embed the template **inline** in the request body, no Base64 wrapping needed.

```typescript
import WebSocket from 'ws';
import { Encoder, decode } from '@msgpack/msgpack';
import { PublicKey } from '@solana/web3.js';

const encoder = new Encoder({ useBigInt64: true });

const CB_PROGRAM_ID = new PublicKey('ComputeBudget111111111111111111111111111111');

function setComputeUnitLimitData(units: number) {
  const data = new Uint8Array(5);
  data[0] = 0x02;
  new DataView(data.buffer).setUint32(1, units, true);
  return data;
}

function setComputeUnitPriceData(microLamports: bigint) {
  const data = new Uint8Array(9);
  data[0] = 0x03;
  new DataView(data.buffer).setBigUint64(1, microLamports, true);
  return data;
}

const template = {
  i: [
    { p: CB_PROGRAM_ID.toBytes(), a: [], d: setComputeUnitLimitData(1_400_000) },
    { p: CB_PROGRAM_ID.toBytes(), a: [], d: setComputeUnitPriceData(0n) },
  ],
  a: [],
  m: [],
};

const ws = new WebSocket(
  `${process.env.TITAN_WS_ENDPOINT}/api/v1/ws`,
  ['v1.api.titan.ag'],
  { headers: { Authorization: `Bearer ${process.env.TITAN_JWT}` } },
);

ws.on('open', () => {
  ws.send(encoder.encode({
    id: 1,
    data: {
      NewSwapQuoteStream: {
        swap: {
          inputMint: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v').toBytes(),
          outputMint: new PublicKey('So11111111111111111111111111111111111111112').toBytes(),
          amount: 100_000_000n,
          slippageBps: 50,
          providers: ['Metis', 'Titan'],
          transactionTemplate: template,   // inline, no Base64
        },
        transaction: {
          userPublicKey: new PublicKey('Affd7LDkUY9fjWjjQSr9bvises1Cku4WSwdLNGBhgVW3').toBytes(),
          titanSwapVersion: 3,             // integer 3
        },
        update: { intervalMs: 60_000, numQuotes: 1 },
      },
    },
  }));
});

ws.on('message', (raw) => {
  const msg = decode(raw as Uint8Array) as any;
  if ('StreamData' in msg && 'SwapQuotes' in msg.StreamData.payload) {
    const quotes = msg.StreamData.payload.SwapQuotes;
    const winner = quotes.metadata?.ExpectedWinner;
    const route = winner && quotes.quotes[winner];
    // Assemble [template.i, ...route.instructions] as in the REST tab
  }
});
```

{% endtab %}
{% endtabs %}

## What comes back

A template does not change the response shape. Each route returns the same [`SwapRoute`](/titan/developer-doc/swap-api/reference/direct/new-swap-quote-stream.md#response) object, with `instructions` containing the swap and `addressLookupTables` containing the ALT addresses the router selected. The template determines which route wins, not the structure of the response.

Constructing the final transaction is left to you. Take the winning route's `instructions` and place your own around them in the order declared in `i` — yours first, the swap after. Because `addressLookupTables` returns keys rather than table contents, fetch each ALT account with `getAddressLookupTable`, then compile a v0 message with your ALT accounts ahead of the route's, matching the order declared in `a`.

Re-request the quote whenever your instruction set changes, since a route sized against the previous template may no longer fit.

## What changes in the route

Without a template, Titan sizes routes assuming the swap is the only thing in the transaction — up to the server's defaults (currently 1168 bytes, 64 accounts).

With a template, Titan **subtracts the template's footprint** from those budgets before choosing a route:

* A compute-budget template (14 bytes, 0 accounts) barely shifts routing — V3 routes usually return as a single instruction with the ALTs the router would have used anyway.
* A larger template (custom program calls, multiple ALTs, many account metas) pushes the router toward shorter routes — fewer hops, fewer venues — to leave room. Providers that can't fit a route within the remaining budget are silently dropped from the response.
* If no provider can fit a route, you get a 404 (Gateway) or an empty `quotes` map (Direct).

## Pitfalls

* **Don't pair with `accountsLimitTotal` / `accountsLimitWritable` / `sizeConstraint`.** The template replaces them. Passing both returns 400.
* **`titanSwapVersion` is integer `3`, not string `"V3"`.** Strings get rejected with `invalid digit found in string`.
* **Use wire-format field names.** Long-form names (`programId`, `accounts`, etc.) return `400 Bad Request: missing field 'p'`.
* **Splice the template instructions BEFORE the route instructions** when building the final v0 message. Titan does not do this for you — the response contains only the swap. The template describes the surrounding transaction; the swap sits after it.
* **ALT order is load-bearing.** Solana resolves ALTs greedily; the first ALT containing an account wins. If your custom ALT and a Titan ALT both contain the same account, ordering decides which gets used.
* **Encode binary as bytes, not Base58.** Inside the MessagePack-encoded template, pubkeys and instruction `data` are raw bytes (msgpack `bin`) — don't pre-encode them to strings.

## Related pages

* [NewSwapQuoteStream → Transaction Template](/titan/developer-doc/swap-api/reference/direct/new-swap-quote-stream.md#transaction-template) — type definition
* [NewSwapQuoteStream → Swap V3](/titan/developer-doc/swap-api/reference/direct/new-swap-quote-stream.md#swap-v3) — V3 router details
* [Quote Swap (Gateway)](/titan/developer-doc/swap-api/reference/gateway/gateway-quote-swap.md) — Gateway swap reference
* [Stream & Execute a Swap](/titan/developer-doc/swap-api/guides/stream-and-execute.md) — full transaction-building walkthrough
* [Configure Routing](/titan/developer-doc/swap-api/guides/configure-routing.md) — `accountsLimitTotal`, `accountsLimitWritable`, and other size controls
