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
| Requirement | Why it matters |
|---|---|
| Swap API key | Authenticates the backend quote request with X-API-KEY |
| RPC key | Submits and confirms through https://rpc.carbium.io/?apiKey=... |
| User wallet address | Lets Q1 build an executable txn for the intended signer |
| Explicit signing boundary | Prevents key leaks and confused custody |
| Request ID | Connects 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_inanddestAmountOutMinare acceptable for the UI or strategyroutePlanis present when your app displays route detailstxnexists 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.
| Stage | Common signal | Best next move |
|---|---|---|
| Quote request | 401, 403, 400, missing txn, no route | Swap API Errors Reference |
| Route review | Output too low, pair mismatch, slippage too wide | Reject the quote and request a new one |
| Wallet signing | User rejection, wallet unavailable, serialization issue | Stop cleanly or ask the user to retry |
| Backend signing | Signer unavailable, policy rejection | Fix signer policy and secret boundary before retry |
| RPC submission | Send failure before signature is accepted | Check RPC auth, preflight, and network response |
| Confirmation | Signature exists but status is failed or unclear | Debug Solana Transaction Simulation and check signature state before replay |
Production checklist
Before shipping a Carbium swap execution flow, verify that:
CARBIUM_API_KEYandCARBIUM_RPC_KEYare backend-onlyuser_accountis present whenever the flow expectstxn- 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
skipPreflightbehavior 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.
Updated about 10 hours ago
