Send Solana transactions reliably

Build, send, rebroadcast and confirm a Solana transaction: fresh blockhash, compute limit, priority fee, a send loop with fallback, and the rule for when it is safe to sign again.

Network
Solana
Authentication
x-token: $TRIPORT_API_KEY on every HTTP call
Required scope
solana:rpc
Tier / rate limit
Every plan, from the 7-day Free trial (free) up; sends use the sol_send_tx budget, reads the sol_read_rpc budget

sendTransaction answers as soon as an upstream accepts your transaction. That answer does not mean it landed. This guide builds the loop that closes the gap: one blockhash, one signed transaction, resent until it confirms or expires, with no way of landing twice.

What you need

  • An API key. Create one in the console. The examples read it from TRIPORT_API_KEY.
  • A plan. sendTransaction is on every plan: 5 requests/s on the Free trial, 10 on Basic, 50 on Pro and 150 on Business. simulateTransaction starts at Pro. On lower plans use the compute figure from error.data.unitsConsumed when preflight fails, or set the limit from your own measurements.
  • A signing library. The examples use signAndSerialize() as a placeholder for yours. Triport never signs for you.

1. Fetch the blockhash and keep its deadline

Read the blockhash at confirmed, and keep both fields. lastValidBlockHeight is the deadline for everything below.

async function rpc(method, params, url = "https://triport.io/sol") {
  const res = await fetch(url, {
    method: "POST",
    headers: { "x-token": process.env.TRIPORT_API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  if (body.error) throw Object.assign(new Error(body.error.message), body.error);
  return body.result;
}


const { value } = await rpc("getLatestBlockhash", [{ commitment: "confirmed" }]);
const { blockhash, lastValidBlockHeight } = value;

A blockhash is valid for 150 blocks. Count blocks, not seconds: slot times change, and a fixed timeout is how a client ends up signing a second transaction while the first can still land.

2. Set the compute limit and the priority fee

Both go in the message as compute budget instructions, before you sign. An instruction added afterwards invalidates the signature.

  1. Get the compute your transaction uses, from a simulation on Pro and above, or from measurements of the same transaction shape.
  2. Set SetComputeUnitLimit to that figure plus about 10%. Your fee is calculated on the limit you request, not on what you use.
  3. Read recent fees for the accounts you write to, and take a percentile.
const samples = await rpc("getRecentPrioritizationFees", [writableAccounts]);
const fees = samples.map((s) => s.prioritizationFee).sort((a, b) => a - b);
const microLamportsPerCu = fees[Math.floor(fees.length * 0.75)] ?? 0;

How to calculate a Solana priority fee has the formula and a worked example. If you add a tip, read what a tip buys first. The account must be in the static keys for Triport to detect it.

3. Sign once, send with a fallback

Sign one transaction and reuse those bytes everywhere below. Set maxRetries: 0, because your loop handles rebroadcasting.

const signed = signAndSerialize({ blockhash, computeUnitLimit, microLamportsPerCu });
const config = { encoding: "base64", preflightCommitment: "confirmed", maxRetries: 0 };


async function send(params) {
  try {
    return await rpc("sendTransaction", params, "https://fra.triport.io/sol");
  } catch (e) {
    if (e.code !== undefined) throw e;           // the node answered: not a transport error
    return rpc("sendTransaction", params);       // the direct lane has no failover
  }
}


const signature = await send([signed, config]);

https://fra.triport.io/sol is for sending only, and has no automatic failover, so the fallback belongs in your code. Keep preflight on until you have a reason to remove it: a -32002 answer gives you the error and logs at once. See transaction simulation failed.

4. Rebroadcast until it confirms or expires

Resend the same bytes on a short interval. Solana processes a signature at most once, so this cannot land twice. Stop when the signature confirms, or when the chain passes lastValidBlockHeight.

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const rebroadcast = { ...config, skipPreflight: true };   // already checked once


while (true) {
  const { value } = await rpc("getSignatureStatuses", [[signature]]);
  const status = value[0];
  if (status?.err) throw new Error(`failed on chain: ${JSON.stringify(status.err)}`);
  if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
    return signature;
  }


  const height = await rpc("getBlockHeight", [{ commitment: "confirmed" }]);
  if (height > lastValidBlockHeight) throw new Error("expired: safe to rebuild");


  await send([signed, rebroadcast]).catch(() => {});
  await sleep(1000);
}

Rebroadcasting spends your sol_send_tx budget, and the status and height checks spend sol_read_rpc. If you would rather not poll, signatureSubscribe on wss://triport.io/ws/sol pushes the result, and it is available on every plan.

5. Sign again only after expiry

This rule prevents double execution. A new signature is a new transaction, and the old one is still valid until its blockhash expires.

What you knowSafe move
Signature confirmedDone
status.err is setIt failed on chain. Rebuilding is a new decision: check why first
Height is at or below lastValidBlockHeight, no statusResend the same bytes
Height is above lastValidBlockHeight, no statusIt can never land. Rebuild, with a new blockhash

Common answers

You seeMeansGo to
-32002Preflight failed, nothing was sentTransaction simulation failed
Blockhash not foundExpired, or not seen yetBlockhash not found
-32003A signature does not match the messageSignature verification failure
HTTP 429Over your sol_send_tx rateHTTP 429
Signature, then nothingDropped or expiredWhy a transaction never lands

Verify it works

  • Log X-Triport-Send-Legs and X-Triport-Send-Winner from each response, to see which paths were used.
  • Count outcomes per signature: confirmed, failed on chain, expired. Expired transactions with a low fee usually point at the fee, and expired ones with a high fee point at delivery.
  • Compare routes over the same period, with the same transactions.

What this does not do

  • It does not make a transaction land. It makes sure you know what happened, and that you never sign twice while the first transaction can still land.
  • It does not cover bundles, which have their own rules.
  • It does not choose a fee for you. A percentile of recent fees is a starting point, not a guarantee.
  • The code is a sketch of the logic. Add your own limits on total attempts and spending.

Next steps