Gasless Token Swap

Build a backend-assisted gasless swap flow with Carbium's documented v1 swap surface while keeping API keys server-side and user signing inside the wallet.

Gasless Token Swap

A gasless token swap is still a wallet-approved Solana transaction. The difference is that your product asks Carbium for a gasless-capable swap build when the user has the input token but does not have enough SOL to pay the normal network fee.

Use this page when you are building the integration path. If you only need the product concept and user-flow decision, start with Gasless Swaps.

Part of the Carbium Solana infrastructure stack.


The safe shape

Do not ask users to paste private keys into a CLI, form, support chat, or .env file. A production gasless flow should keep three boundaries separate:

LayerOwnsMust not contain
Wallet clientuser consent and transaction signingCarbium API keys
Your backendCarbium Swap API key, request policy, logs, relay coordinationuser private keys
Carbium Swap APIbuilding the swap transaction payloaduser custody

The wallet signs. Your backend requests the transaction and relays the signed payload. Carbium builds the transaction for the documented route.

🔶

Never design a gasless flow around exported private keys. If a user has to paste a private key, the integration is solving the wrong problem.


Where gasless fits

Use gasless only when it solves the no-SOL fee problem. It is not a universal default for every route.

Good fits:

  • a wallet user holds a token but no SOL
  • an onboarding flow needs one successful swap before the user can fund SOL normally
  • a mobile or embedded app wants fewer off-product funding steps
  • a support path needs to distinguish "no SOL for fees" from normal swap failures

Use the normal quote and swap path when the user already has enough SOL for fees or when the route does not support gasless handling.


Request the transaction from your backend

The documented gasless flag lives on the older provider-specific v1 swap builder:

GET https://api.carbium.io/api/v1/swap

That surface uses the v1 parameter family: owner, fromMint, toMint, amount, slippage, provider, and optional execution fields such as gasless.

A backend route can build the request like this:

const CARBIUM_SWAP_BASE = "https://api.carbium.io/api/v1/swap";

type GaslessSwapInput = {
  owner: string;
  fromMint: string;
  toMint: string;
  amount: string;
  slippageBps: string;
  provider: string;
};

export async function buildGaslessSwap(input: GaslessSwapInput) {
  const url = new URL(CARBIUM_SWAP_BASE);
  url.searchParams.set("owner", input.owner);
  url.searchParams.set("fromMint", input.fromMint);
  url.searchParams.set("toMint", input.toMint);
  url.searchParams.set("amount", input.amount);
  url.searchParams.set("slippage", input.slippageBps);
  url.searchParams.set("provider", input.provider);
  url.searchParams.set("gasless", "true");

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

  if (!response.ok) {
    throw new Error(`Gasless swap build failed: ${response.status}`);
  }

  return response.json();
}

Keep the Swap API key in a backend environment variable or secret manager. The browser or mobile client should send user intent and a wallet public key, not the Carbium credential.


Return only the unsigned transaction payload

The v1 swap guide documents a base64 transaction payload. Different response paths may wrap the payload in data, base64Transaction, or another transaction field, so normalize the response before returning it to the client.

function extractBase64Transaction(payload: any): string {
  const candidate =
    payload?.data?.base64Transaction ??
    payload?.data?.transaction ??
    payload?.base64Transaction ??
    payload?.transaction ??
    payload?.data;

  if (typeof candidate !== "string" || candidate.length === 0) {
    throw new Error("Swap response did not include a base64 transaction");
  }

  return candidate;
}

export async function handleGaslessSwapRequest(req: Request) {
  const input = await req.json();
  const payload = await buildGaslessSwap(input);
  const transactionBase64 = extractBase64Transaction(payload);

  return Response.json({
    transactionBase64,
  });
}

Before returning a transaction to the wallet, your backend should validate the request policy: allowed providers, supported token pairs, maximum size, slippage bounds, and any app-specific risk checks.


Let the wallet sign

The client receives a base64 transaction payload and asks the connected wallet to sign it.

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

async function signGaslessSwap(transactionBase64: string, wallet: any) {
  const transaction = VersionedTransaction.deserialize(
    Buffer.from(transactionBase64, "base64")
  );

  const signed = await wallet.signTransaction(transaction);

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

The user still reviews the wallet prompt. Gasless changes the fee path; it does not remove consent.

If the wallet rejects the prompt, stop the flow. Do not rebuild and retry automatically unless the user explicitly starts a new attempt.


Relay and confirm server-side

After signing, send the serialized signed transaction back to your backend. Relay it through Carbium RPC or your chosen Solana submission path, then confirm before marking the swap complete.

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

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

export async function submitSignedSwap(signedBase64: string) {
  const transaction = VersionedTransaction.deserialize(
    Buffer.from(signedBase64, "base64")
  );

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

  const confirmation = await connection.confirmTransaction(
    signature,
    "confirmed"
  );

  return { signature, confirmation };
}

Persist the signature and request ID before retrying. If a worker restarts or the user refreshes, check signature status first instead of building a second transaction for the same intent.


Failure handling

A gasless branch needs clearer errors than a normal swap button because several systems are involved.

SymptomLikely boundaryFirst check
401 or 403 from Swap APIBackend authConfirm X-API-KEY uses the Swap API key, not the RPC key
Route cannot be builtSwap routeCheck provider, pair, amount, slippage, and whether gasless is supported for that path
No transaction payload returnedBuild responseConfirm the response shape and route support before sending anything to the wallet
Wallet rejects signingUser consentStop cleanly and let the user start over
Signed transaction fails or times outRPC or on-chain executionCheck transaction logs, blockhash age, confirmation status, and retry policy

Use Swap API Errors Reference for pre-sign Swap API failures and Debug Solana Transaction Simulation for simulation or preflight problems.


Production checklist

Before shipping a gasless swap integration, verify:

  • the Swap API key never reaches browser or mobile code
  • users never paste or export private keys
  • gasless=true is enabled only on the branch that needs fee assistance
  • the UI explains output amount, token mints, and signing intent before the wallet prompt
  • route misses are surfaced as route availability, not generic downtime
  • signed transactions are submitted once and tracked by signature
  • support logs separate quote/build failure, wallet rejection, RPC submission, and on-chain failure
🔶

Build gasless as a backend-assisted wallet flow: Carbium builds, the wallet signs, your backend relays, and the user stays in control. Start broader product setup at carbium.io.