# Sending Transactions

> Sending, retrying and confirming Solana transactions, the staked SWQoS path and priority fees.

Source: https://docs.constant-k.com/rpc/sending-transactions/

`sendTransaction` forwards a signed transaction to the next four leaders in the schedule and rebroadcasts it every 420 ms. The call works as it does on any Solana RPC.

## Network and staked paths

| Plan         | Network path | Staked path (SWQoS) |
| :----------- | :----------: | :-----------------: |
| Free         |      1/s     |          —          |
| Operator     |     15/s     |         1/s         |
| Blade Runner |     50/s     |         5/s         |
| Nexus        |     200/s    |         20/s        |

Network is the default path and is available on every plan. Staked sends through stake-weighted quality of service: a leader reserves 80% of its inbound transaction capacity for connections from staked validators and divides that share in proportion to stake, so staked traffic is less likely to be dropped when the leader is busy. Solana's [Stake-weighted Quality of Service](https://solana.com/docs/defi/stake-weighted-qos) guide covers the mechanism.

Both figures are a rate per second, not a monthly allowance. There is no monthly cap on either path, and nothing accrues or expires. Limits are counted per region, so a key holds the full rate in New York and the full rate in Frankfurt at the same time. Every `sendTransaction` call counts, including rebroadcasts.

The [Benchmarks](https://www.constant-k.com/benchmarks/) page shows live landing times and success rates for both paths.

## Staked endpoints

The staked path is selected by hostname. There is no header, no extra parameter and no request to make first.

```text
https://staked-rpc-newyork.constant-k.com/?api-key=YOUR_API_KEY
https://staked-rpc-frankfurt.constant-k.com/?api-key=YOUR_API_KEY
https://staked-rpc-amsterdam.constant-k.com/?api-key=YOUR_API_KEY
```

The staked hostnames work on Operator, Blade Runner and Nexus, and serve one transaction per call. Every other method works there too, against the same limits as the regular RPC hostname, so `getLatestBlockhash` and `getSignatureStatuses` calls do not have to move.

> **maxRetries and skipPreflight are fixed on staked hostnames:** `maxRetries` is always `0` and `skipPreflight` is always `true`. Setting them has no effect, so rebroadcasting is the client's own job, as in the example below. Everything else, including the transaction, `encoding` and `minContextSlot`, is passed through unchanged.

### Which path a send took

Every response from a staked hostname carries an `X-CK-Tx-Route` header. It is the only way to confirm that a send went over SWQoS.

```bash
curl -sS -D - -o /dev/null "https://staked-rpc-newyork.constant-k.com/?api-key=YOUR_API_KEY" \
  -X POST -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"sendTransaction","params":["BASE64_SIGNED_TRANSACTION",{"encoding":"base64"}]}' \
  | grep -i "x-ck-tx-route"
```

| Value                              | Path taken                                                                 |
| :--------------------------------- | :------------------------------------------------------------------------- |
| Any value that is not listed below | The staked route. The value names it.                                      |
| Any value starting with `local_`   | The constant-k node. The suffix gives the reason, below.                   |
| `passthrough`                      | The request was not a `sendTransaction`, so the staked path did not apply. |

> **The header reports the route chosen, not the outcome at the leader:** A staked value means the transaction was handed to the staked route. If that route then returns an error, the transaction is sent from the constant-k node instead, and the response still carries the staked value, because the header is written before that happens. No RPC can report whether a leader included a transaction; `getSignatureStatuses` is what settles that.

### When a staked send falls back

A send that cannot take the staked path is not refused. The host sends the transaction from the constant-k node instead and returns the signature as usual, so a fallback is visible only in the header.

| Value                     | Reason                                                                                                                               |
| :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------- |
| `local_tier`              | The plan does not include the staked path.                                                                                           |
| `local_cap_key`           | Over the plan's staked sends per second for that region.                                                                             |
| `local_cap_site`          | The region's shared staked capacity is full.                                                                                         |
| `local_budget`            | constant-k's own staked budget for the region is spent for the month. This is not a per-customer allowance.                          |
| `local_dedup`             | The same encoded transaction was sent in the last 60 seconds. See [Resending the same transaction](#resending-the-same-transaction). |
| `local_badparams`         | The transaction parameter is not a string.                                                                                           |
| `local_oversize`          | The request body is over 8 KB.                                                                                                       |
| `local_off`, `local_hold` | The staked route is not taking sends at the moment.                                                                                  |

### Resending the same transaction

The staked path serves a repeat of the same transaction from the constant-k node, so one transaction is never handed to the staked route twice. The window is 60 seconds, about the life of a blockhash.

The match is on the encoded transaction string as sent. A signed transaction always serializes to the same bytes and the same signature, so a rebroadcast matches and is safe by construction. Re-signing against a fresh blockhash produces a different transaction, which takes the staked path again. The same transaction re-encoded from base58 to base64, or the other way, does not match.

Re-signing while the previous transaction can still land is the hazard this does not cover: two transactions with different blockhashes are two transactions, and both can execute. The old blockhash has to expire first.

A transaction served this way is still sent, and the node rebroadcasts it as described in [Rebroadcasting](#rebroadcasting).

### Rebroadcasting

The staked route takes one shot: no retry and no rebroadcast. That is why `maxRetries` is forced to `0` on staked hostnames, and why the client owns retries there.

The constant-k node behaves differently. These settings are the same in every region.

| Setting                                 | Value                      |
| :-------------------------------------- | :------------------------- |
| Leaders a send is forwarded to          | The next 4 in the schedule |
| Rebroadcast interval                    | 420 ms                     |
| Rebroadcasts when `maxRetries` is unset | 2                          |
| Highest `maxRetries` the node honours   | 5                          |

Which of the two applies depends on how the send was routed.

| Send                              | Rebroadcast by                                                                                                                                                                       |
| :-------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Any send on a regular hostname    | The node, on the schedule above, unless `maxRetries: 0` is set.                                                                                                                      |
| A staked hostname, `local_` value | The node, on the schedule above. `maxRetries` is passed through, so the client's own value applies.                                                                                  |
| A staked hostname, staked value   | Nobody. `maxRetries` was rewritten to `0` before the send, and that rewritten value also applies if the staked route errors and the transaction is re-sent from the constant-k node. |

A client that sets `maxRetries: 0` and rebroadcasts on its own, as the example below does, replaces this entirely. A client loop makes the interval and the stopping condition explicit, and does not share the node's retry queue with every other caller when the network is busy.

## A send-and-confirm loop

This example transfers 0.001 SOL. It sets a priority fee, sends without preflight, rebroadcasts until the transaction confirms, and stops when the blockhash expires.

> **Plan required:** These examples call `getLatestBlockhash` and `getSignatureStatuses`, which need the Operator plan or higher.

**TypeScript**

```bash
npm install @solana/kit @solana-program/system @solana-program/compute-budget
```

```ts title="send.ts"
import { readFileSync } from "node:fs";
import {
  address,
  appendTransactionMessageInstructions,
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createTransactionMessage,
  getBase64EncodedWireTransaction,
  getSignatureFromTransaction,
  lamports,
  pipe,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  signTransactionMessageWithSigners,
} from "@solana/kit";
import {
  getSetComputeUnitLimitInstruction,
  getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
import { getTransferSolInstruction } from "@solana-program/system";

const rpc = createSolanaRpc(
  `https://rpc-newyork.constant-k.com/?api-key=${process.env.CK_API_KEY}`,
);

// A Solana CLI keypair file (JSON array of 64 bytes)
const payer = await createKeyPairSignerFromBytes(
  new Uint8Array(JSON.parse(readFileSync(process.env.KEYPAIR_PATH!, "utf8"))),
);

// 1. Get a recent blockhash
const { value: latestBlockhash } = await rpc
  .getLatestBlockhash({ commitment: "confirmed" })
  .send();

// 2. Build the message: compute budget first, then your instructions
const message = pipe(
  createTransactionMessage({ version: 0 }),
  (m) => setTransactionMessageFeePayerSigner(payer, m),
  (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
  (m) =>
    appendTransactionMessageInstructions(
      [
        getSetComputeUnitLimitInstruction({ units: 1_000 }),
        getSetComputeUnitPriceInstruction({ microLamports: 10_000n }),
        getTransferSolInstruction({
          source: payer,
          destination: address("RECIPIENT_ADDRESS"),
          amount: lamports(1_000_000n), // 0.001 SOL
        }),
      ],
      m,
    ),
);

// 3. Sign and serialize
const signedTx = await signTransactionMessageWithSigners(message);
const signature = getSignatureFromTransaction(signedTx);
const wireTx = getBase64EncodedWireTransaction(signedTx);

// 4. Send, then rebroadcast until it confirms or the blockhash expires
const sendOptions = { encoding: "base64", skipPreflight: true, maxRetries: 0n } as const;

while (true) {
  await rpc.sendTransaction(wireTx, sendOptions).send();
  await new Promise((resolve) => setTimeout(resolve, 400));

  const { value: [status] } = await rpc.getSignatureStatuses([signature]).send();
  if (status?.err) {
    throw new Error(`Transaction ${signature} failed`, { cause: status.err });
  }
  if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
    console.log("Confirmed:", signature);
    break;
  }

  const blockHeight = await rpc.getBlockHeight({ commitment: "confirmed" }).send();
  if (blockHeight > latestBlockhash.lastValidBlockHeight) {
    throw new Error(`Blockhash expired before ${signature} landed. Rebuild and resend.`);
  }
}
```

The script runs with `KEYPAIR_PATH=~/.config/solana/id.json npx tsx send.ts`.

**cURL**

A transaction already signed and serialized as base64:

```bash
curl "https://rpc-newyork.constant-k.com/?api-key=YOUR_API_KEY" \
  -X POST -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sendTransaction",
    "params": [
      "BASE64_SIGNED_TRANSACTION",
      {"encoding": "base64", "skipPreflight": true, "maxRetries": 0}
    ]
  }'
```

The result is the transaction signature. Its status:

```bash
curl "https://rpc-newyork.constant-k.com/?api-key=YOUR_API_KEY" \
  -X POST -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getSignatureStatuses",
    "params": [["TRANSACTION_SIGNATURE"]]
  }'
```

**Python**

```python title="send.py"
import base64
import os
import time

import requests

URL = f"https://rpc-newyork.constant-k.com/?api-key={os.environ['CK_API_KEY']}"

def rpc(method, params):
    body = requests.post(
        URL,
        json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
        timeout=10,
    ).json()
    if "error" in body:
        raise RuntimeError(body["error"])
    return body["result"]

def send_and_confirm(signed_tx: bytes, timeout_s: float = 30.0) -> str:
    """signed_tx: a signed, serialized transaction, e.g. bytes(tx) from solders."""
    wire_tx = base64.b64encode(signed_tx).decode()
    opts = {"encoding": "base64", "skipPreflight": True, "maxRetries": 0}

    signature = rpc("sendTransaction", [wire_tx, opts])
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        time.sleep(0.4)
        status = rpc("getSignatureStatuses", [[signature]])["value"][0]
        if status and status["err"]:
            raise RuntimeError(f"{signature} failed: {status['err']}")
        if status and status["confirmationStatus"] in ("confirmed", "finalized"):
            return signature
        rpc("sendTransaction", [wire_tx, opts])  # rebroadcast while waiting
    raise TimeoutError(f"{signature} not confirmed after {timeout_s}s")
```

### Confirmation over WebSocket

With a [WebSocket](https://docs.constant-k.com/rpc/websockets/) connection, `@solana/kit` waits for confirmation instead of polling:

```ts
import {
  assertIsTransactionWithBlockhashLifetime,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  getSignatureFromTransaction,
  sendAndConfirmTransactionFactory,
} from "@solana/kit";

const key = process.env.CK_API_KEY;
const rpc = createSolanaRpc(`https://rpc-newyork.constant-k.com/?api-key=${key}`);
const rpcSubscriptions = createSolanaRpcSubscriptions(
  `wss://ws-newyork.constant-k.com:1337/ws?api-key=${key}`,
);

const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

// signedTx: built and signed as in the example above
assertIsTransactionWithBlockhashLifetime(signedTx);
await sendAndConfirmTransaction(signedTx, {
  commitment: "confirmed",
  skipPreflight: true,
});
console.log("Confirmed:", getSignatureFromTransaction(signedTx));
```

## Priority fees

Leaders order transactions partly by priority fee. On legacy and version 0 transactions it is a price per compute unit, in micro-lamports, set with a `SetComputeUnitPrice` instruction. A realistic compute unit limit alongside it avoids paying for units the transaction does not use. Version 1 sets the fee as a total in lamports; see [Version 1 transactions](#version-1-transactions).

A price comes from what recent transactions paid to write to the same accounts.

**TypeScript**

```ts
import { address, createSolanaRpc } from "@solana/kit";

const rpc = createSolanaRpc(
  `https://rpc-newyork.constant-k.com/?api-key=${process.env.CK_API_KEY}`,
);

// Fees recently paid by transactions that wrote to these accounts
const fees = await rpc
  .getRecentPrioritizationFees([address("JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4")])
  .send();

const sorted = fees.map((f) => f.prioritizationFee).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
const p75 = sorted[Math.floor(sorted.length * 0.75)] ?? 0n;
console.log(`75th percentile: ${p75} micro-lamports per CU`);
```

**cURL**

```bash
curl "https://rpc-newyork.constant-k.com/?api-key=YOUR_API_KEY" \
  -X POST -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getRecentPrioritizationFees",
    "params": [["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"]]
  }'
```

## Version 1 transactions

`sendTransaction` takes the signed bytes and does not care which format they are, so sending version 1 needs no parameter or hostname change. constant-k nodes run `solana-core` 4.2.2 or later and serve it. Four things change in how you build the transaction:

* The signing library decides the format: `@solana/kit` 8.0.0 or later, or the Rust `solana-*` crates at 4.2.
* The priority fee is a total in lamports in the message config, not a price per compute unit in a ComputeBudget instruction.
* An unset config field means the minimum, not a default, so the compute unit limit and the loaded accounts data size have to be set explicitly.
* Address lookup tables do not exist in version 1. A transaction that needs one stays on version 0.

[Transaction v1](https://docs.constant-k.com/rpc/transaction-v1/) covers the message config, the limits, the client minimums and reading these transactions back.

## Client rules

| Rule                                                   | Detail                                                                                                                                                                                                          |
| :----------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A fresh blockhash                                      | Fetched with `confirmed` commitment immediately before signing. `lastValidBlockHeight` says when to stop retrying.                                                                                              |
| Preflight off in production                            | `skipPreflight: true` drops a simulation before the send. In development, `simulateTransaction` is a separate call with its own lower rate limit.                                                               |
| Client-owned retries                                   | `maxRetries: 0`, with the client rebroadcasting the same signed transaction until it confirms or the blockhash expires. Left unset, the node rebroadcasts twice instead. See [Rebroadcasting](#rebroadcasting). |
| No re-signing while the old transaction can still land | A new blockhash makes a new transaction, and both can execute. The old blockhash has to expire first.                                                                                                           |
| One region per flow                                    | Send and confirm on the same region. Nodes can differ by a slot or two. See [Choosing a region](https://docs.constant-k.com/get-started/endpoints/#choosing-a-region).                                                                     |
| Distance to the node                                   | Your latency to the node adds directly to landing time.                                                                                                                                                         |
| Rebroadcasts count                                     | Every rebroadcast counts toward your `sendTransaction` limit. A 400 ms loop sends about 2.5 calls per second per transaction.                                                                                   |

## What affects landing

No RPC can guarantee that a transaction lands. These factors decide it:

| Factor             | Effect                                                                                                                                                                                |
| :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Priority fee       | Ranked against other transactions that want the same accounts.                                                                                                                        |
| Distance           | From your sender to the node, and from the node to the current leader. The leader changes every four slots, about 1.1 seconds at current slot times, so this distance keeps changing. |
| Blockhash age      | An old blockhash leaves less time to land.                                                                                                                                            |
| Compute unit limit | On legacy and version 0, an oversized limit lowers your priority per unit. On version 1 the fee is a total, so the limit changes your cost, not your price per unit.                  |

## Tips and bundles

`sendBundle` is not supported. `sendTransaction` takes single transactions.

Stake-weighted delivery, a priority fee and a tip act at different stages, so one does not substitute for another.

| Mechanism              | What it decides                                                              |
| :--------------------- | :--------------------------------------------------------------------------- |
| Staked path            | Whether the leader accepts the connection when its inbound capacity is full. |
| Priority fee           | Where the transaction ranks once the leader holds it.                        |
| Tip to a block builder | Whether several transactions land together and in order, or not at all.      |

A staked send carrying no meaningful compute unit price still loses to transactions that pay one, because reaching the leader and being chosen by the leader are separate problems. Atomicity across more than one transaction is the one thing neither the staked path nor a priority fee provides.

### The same transaction through several providers

Only one copy can land. The others fail as duplicates, or, on a durable nonce, fail once the nonce advances. That is the expected outcome rather than a fault in any one path. `X-CK-Tx-Route` reports the path constant-k used, not which provider's copy won.

## Related

- [sendTransaction](https://docs.constant-k.com/rpc/http/sendtransaction/)

- [simulateTransaction](https://docs.constant-k.com/rpc/http/simulatetransaction/)

- [Errors](https://docs.constant-k.com/rpc/errors/)

- [Transaction v1](https://docs.constant-k.com/rpc/transaction-v1/)
