Executing Swaps

Execute a Carbium swap from executable Q1 quote to wallet approval, RPC submission, confirmation, and production retry handling without exposing API keys or signing material.

Executing Swaps

A Carbium swap is not finished when Q1 returns a route. The safe execution path has four separate jobs: request an executable quote, get the returned transaction signed on the right boundary, submit the signed payload through RPC, and confirm the signature before retrying or marking the swap complete.

This page owns that end-to-end execution path. Use Q1 for the request shape, Swap API Errors Reference for quote-stage failures, and Sending Transactions through Carbium RPC when you only need the relay step.

Part of the Carbium Solana infrastructure stack.


Execution model

For most wallet, bot, and backend integrations, the stable shape is:

flowchart TD
    A["Backend requests Q1 quote<br/>api.carbium.io/api/v2/quote"] --> B["Q1 returns routePlan + txn"]
    B --> C["App validates route and amounts"]
    C --> D{"Signing boundary"}
    D -->|Wallet flow| E["User wallet signs transaction"]
    D -->|Controlled executor| F["Backend signer approves transaction"]
    E --> G["Submit signed bytes through RPC"]
    F --> G
    G --> H["Confirm signature status"]
    H --> I["Complete, wait, or investigate"]

The important rule is simple: Carbium credentials stay on backend infrastructure. The signer approves the transaction, but it does not need the Swap API key or the RPC key.


What you need before executing

RequirementWhy it matters
Swap API keyAuthenticates the backend quote request with X-API-KEY
RPC keySubmits and confirms through https://rpc.carbium.io/?apiKey=...
User wallet addressLets Q1 build an executable txn for the intended signer
Explicit signing boundaryPrevents key leaks and confused custody
Request IDConnects quote, signing, relay, and support logs
📘

This guide intentionally avoids copy-paste private-key loading. If your service uses a controlled backend signer, keep that signer behind your own secret-management boundary and expose only a narrow signing function to the execution flow.


Step 1: Request an executable Q1 quote

Request the quote on your backend so the Swap API key never reaches browser or mobile code.

type ExecutableQuote = {
  srcAmountIn: string;
  destAmountOut: string;
  destAmountOutMin: string;
  priceImpactPct?: string;
  routePlan?: unknown[];
  txn?: string;
};

async function getExecutableQuote(input: {
  inputMint: string;
  outputMint: string;
  amountIn: string;
  slippageBps: string;
  userPublicKey: string;
}): Promise<ExecutableQuote> {
  const url = new URL("https://api.carbium.io/api/v2/quote");

  url.searchParams.set("src_mint", input.inputMint);
  url.searchParams.set("dst_mint", input.outputMint);
  url.searchParams.set("amount_in", input.amountIn);
  url.searchParams.set("slippage_bps", input.slippageBps);
  url.searchParams.set("user_account", input.userPublicKey);

  const response = await fetch(url, {
    headers: {
      "X-API-KEY": process.env.CARBIUM_API_KEY!,
      "Accept": "application/json",
    },
  });

  if (!response.ok) {
    throw new Error("Quote failed with HTTP " + response.status);
  }

  const quote = (await response.json()) as ExecutableQuote;

  if (!quote.txn) {
    throw new Error("Q1 response did not include an executable transaction");
  }

  return quote;
}

Validate the quote before asking anyone to sign it:

  • input and output mint match the user intent
  • amount_in and destAmountOutMin are acceptable for the UI or strategy
  • routePlan is present when your app displays route details
  • txn exists before the flow leaves the backend quote stage

If the request returns 401, 403, no route, or an empty txn, stay in the Swap API layer and use Swap API Errors Reference.


Step 2: Sign on the approved boundary

The quote response contains a base64 transaction payload. Deserialize it, sign it where your app is designed to sign, and return serialized signed bytes to the relay step.

Wallet approval flow

Use this shape for consumer wallets, embedded wallet apps, and any flow where the user must approve the swap.

import { VersionedTransaction } from "@solana/web3.js";

async function signWithWallet(
  transactionBase64: string,
  wallet: {
    signTransaction(tx: VersionedTransaction): Promise<VersionedTransaction>;
  }
) {
  const transaction = VersionedTransaction.deserialize(
    Buffer.from(transactionBase64, "base64")
  );

  const signed = await wallet.signTransaction(transaction);

  return Buffer.from(signed.serialize()).toString("base64");
}

In this flow, the client handles user approval and signing only. The backend still owns the Carbium API key, RPC key, submission policy, and confirmation record.

Controlled backend executor

Use a controlled backend signer only when your product is supposed to own execution, such as a custodial service, internal executor, or trading backend. Keep signer construction outside the request handler.

import { VersionedTransaction } from "@solana/web3.js";

type BackendSigner = {
  signTransaction(tx: VersionedTransaction): Promise<VersionedTransaction>;
};

async function signWithBackendExecutor(
  transactionBase64: string,
  signer: BackendSigner
) {
  const transaction = VersionedTransaction.deserialize(
    Buffer.from(transactionBase64, "base64")
  );

  const signed = await signer.signTransaction(transaction);

  return Buffer.from(signed.serialize()).toString("base64");
}

Do not pass seed phrases, exported private keys, or raw secret arrays through this execution code. If your signing system is not already isolated, build that boundary before automating swaps.


Step 3: Submit through Carbium RPC

Once you have a signed payload, submit it through Carbium RPC from your backend.

import { Connection, VersionedTransaction } from "@solana/web3.js";

const connection = new Connection(
  "https://rpc.carbium.io/?apiKey=" + process.env.CARBIUM_RPC_KEY,
  "confirmed"
);

async function submitSignedSwap(signedTransactionBase64: string) {
  const signedTransaction = VersionedTransaction.deserialize(
    Buffer.from(signedTransactionBase64, "base64")
  );

  const signature = await connection.sendRawTransaction(
    signedTransaction.serialize(),
    {
      skipPreflight: false,
      maxRetries: 3,
    }
  );

  return signature;
}

Use skipPreflight: false as the conservative default while integrating. If a latency-sensitive backend later changes that setting, make it an explicit production decision and keep simulation/debug tooling nearby.


Step 4: Confirm before replay

A submitted signature is the handoff into confirmation, not proof of a completed swap. Store the signature with your app-level request ID before any retry loop can run.

import type { Connection, TransactionSignature } from "@solana/web3.js";

async function confirmSwap(
  connection: Connection,
  signature: TransactionSignature
) {
  const confirmation = await connection.confirmTransaction(
    signature,
    "confirmed"
  );

  if (confirmation.value.err) {
    throw new Error(
      "Transaction failed: " + JSON.stringify(confirmation.value.err)
    );
  }

  return confirmation;
}

For production systems, keep one retry rule above all others: check the existing signature state before rebuilding or resubmitting a swap intent. Blind replay can create duplicate sends, confusing support trails, and worse user outcomes.


Minimal end-to-end server flow

This example shows the orchestration without embedding private-key handling into the guide.

async function executeSwap(input: {
  requestId: string;
  inputMint: string;
  outputMint: string;
  amountIn: string;
  slippageBps: string;
  userPublicKey: string;
  signTransaction(transactionBase64: string): Promise<string>;
}) {
  const quote = await getExecutableQuote({
    inputMint: input.inputMint,
    outputMint: input.outputMint,
    amountIn: input.amountIn,
    slippageBps: input.slippageBps,
    userPublicKey: input.userPublicKey,
  });

  const signedBase64 = await input.signTransaction(quote.txn!);
  const signature = await submitSignedSwap(signedBase64);

  await saveSubmittedSignature({
    requestId: input.requestId,
    signature,
    inputMint: input.inputMint,
    outputMint: input.outputMint,
    amountIn: input.amountIn,
  });

  const confirmation = await confirmSwap(connection, signature);

  return {
    signature,
    confirmation,
    expectedOutput: quote.destAmountOut,
    minimumOutput: quote.destAmountOutMin,
  };
}

saveSubmittedSignature is deliberately app-owned. Persist to the same database or job system that owns your user order, swap intent, or bot execution record.


Failure map

Use the boundary where the failure happened to choose the next debugging page.

StageCommon signalBest next move
Quote request401, 403, 400, missing txn, no routeSwap API Errors Reference
Route reviewOutput too low, pair mismatch, slippage too wideReject the quote and request a new one
Wallet signingUser rejection, wallet unavailable, serialization issueStop cleanly or ask the user to retry
Backend signingSigner unavailable, policy rejectionFix signer policy and secret boundary before retry
RPC submissionSend failure before signature is acceptedCheck RPC auth, preflight, and network response
ConfirmationSignature exists but status is failed or unclearDebug Solana Transaction Simulation and check signature state before replay

Production checklist

Before shipping a Carbium swap execution flow, verify that:

  • CARBIUM_API_KEY and CARBIUM_RPC_KEY are backend-only
  • user_account is present whenever the flow expects txn
  • the user or executor signs only the transaction payload, not API credentials
  • submitted signatures are stored before retry logic runs
  • quote-stage logs and RPC-stage logs are separate but linked by request ID
  • skipPreflight behavior is intentional and documented
  • failed confirmations are investigated before rebuilding the same swap intent
🔶

Use this page for execution, Q1 for the quote payload, and Sending Transactions through Carbium RPC for the relay boundary. Start product setup at carbium.io.