> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitflare.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Transaction v1 on Apex

> OrbitFlare Apex accepts Solana transaction v1 (SIMD-0385) up to 4096 bytes on sendTransaction, the HTTP send routes, and QUIC. Learn how to serialize it.

## Size Limits by Format

Transaction v1 is the message format introduced by SIMD-0385. Its main change for senders is size: a v1 transaction can be up to **4096 bytes**, where legacy and v0 transactions stay at 1232.

| Format | Maximum size   | Accepted on                                                      |
| ------ | -------------- | ---------------------------------------------------------------- |
| Legacy | 1232 bytes     | Every route                                                      |
| v0     | 1232 bytes     | Every route                                                      |
| **v1** | **4096 bytes** | `sendTransaction`, `/send`, `/send-bin`, `/send-batch`, and QUIC |

Apex detects the format from the transaction bytes. There is no flag to set and no separate route.

<Note>
  [Bundle](/apex/bundles) members are limited to 1232 bytes each, whatever their format.
</Note>

A legacy or v0 transaction over 1232 bytes, or any transaction over 4096 bytes, is rejected as too large. See [Errors and rate limits](/apex/errors-and-rate-limits).

## Serialize v1 with the Canonical Encoder

A v1 transaction must be serialized with the canonical encoder for the format. In Rust that is `wincode`. The older bincode and serde path produces the right bytes for legacy and v0, but **the wrong bytes for v1**, and the transaction will be rejected as malformed.

The Rust client exposes the correct encoder as `apex_sender_client::serialize_transaction`. It is byte-identical to bincode for legacy and v0, and correct for v1. `ApexSenderClient::send_transaction` uses it internally, so you only call it yourself when you need the bytes, for example for the HTTP routes.

In other languages, use a Solana library version that implements SIMD-0385 serialization. Check that the first byte of your serialized message is the v1 version prefix before you send.

## The Tip Rule Is the Same

A v1 transaction needs the same [tip](/apex/tips): one top-level SystemProgram transfer to a published tip account, funded by a signer, at or above your tier's floor.

One difference is the compute budget. In a v1 message it lives in the message's own `TransactionConfig`, not in ComputeBudget program instructions. Every limit you leave unset in `TransactionConfig` is 0, including the loaded accounts data size. In our mainnet test on 2026-09-19, a v1 transaction that relied on ComputeBudget instructions landed and then failed on chain with `MaxLoadedAccountsDataSizeExceeded`.

So set all three: the compute unit limit, the loaded accounts data size limit, and, for priority, the fee. The priority fee is a total in lamports, not a price per compute unit. With that config, v1 transactions of 276, 741, and 1641 bytes landed on mainnet through Apex over QUIC. The 1641 byte one is above the legacy 1232 byte limit.

## Rust Example

This program builds a v1 transaction with a 3,000 byte memo-style payload, which would not fit in a legacy transaction, and sends it over QUIC. It needs `solana-message = "=4.4.1"` in addition to the [Quickstart dependencies](/apex/quickstart#rust-dependencies).

```rust theme={null}
use std::str::FromStr;
use std::time::Duration;

use apex_sender_client::rpc::{RpcClient, SolanaRpc};
use apex_sender_client::{
    serialize_transaction, tip, tip_instruction, ApexSenderClient, Region, MIN_TIP_LAMPORTS,
};
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::read_keypair_file;
use solana_message::v1::{self, TransactionConfig};
use solana_message::VersionedMessage;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("APEX_API_KEY")?;
    let solana = SolanaRpc::new(
        std::env::var("SOLANA_RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into()),
    );
    let payer = read_keypair_file(std::env::var("KEYPAIR_PATH").unwrap_or_else(|_| "payer.json".into()))?;
    let region = Region::Frankfurt;

    let client = ApexSenderClient::connect(region, &api_key).await?;
    let tip_accounts = RpcClient::new(region, &api_key).get_tip_accounts().await?;
    let tip_account = tip::pick_tip_account(&tip_accounts).ok_or("no tip accounts")?;

    // A payload far larger than a legacy transaction could carry.
    let memo = Instruction::new_with_bytes(
        Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")?,
        "v".repeat(3_000).as_bytes(),
        vec![AccountMeta::new(payer.pubkey(), true)],
    );
    let instructions = [
        memo,
        tip_instruction(&payer.pubkey(), &tip_account, MIN_TIP_LAMPORTS),
    ];

    // v1 sets the compute budget in the message config. Every limit left unset is 0.
    let config = TransactionConfig::empty()
        .with_compute_unit_limit(400_000)
        .with_loaded_accounts_data_size_limit(1024 * 1024)
        .with_priority_fee(4_000); // total lamports, not a price per compute unit
    let message = v1::Message::try_compile_with_config(
        &payer.pubkey(),
        &instructions,
        solana.latest_blockhash().await?,
        config,
    )?;
    let tx = VersionedTransaction::try_new(VersionedMessage::V1(message), &[&payer])?;

    let wire = serialize_transaction(&tx)?;
    println!("v1 transaction: {} bytes, prefix {:#04x}", wire.len(), wire[0]);
    assert_eq!(wire[0], v1::V1_PREFIX);

    let signature = client.send_transaction(&tx).await?;
    println!("sent: {signature}");
    println!("slot: {:?}", solana.confirm(&signature.to_string(), Duration::from_secs(30)).await?);
    Ok(())
}
```

Sending more than 4096 bytes fails locally with `Error::TooLarge` before anything reaches the network.

## Sending v1 over HTTP

Once you hold correctly serialized v1 bytes, the HTTP routes take them exactly as they take any transaction: base64 for `sendTransaction` and `/send`, raw bytes for `/send-bin` and `/send-batch`.

```rust theme={null}
// `tx` is the v1 transaction from the example above.
use apex_sender_client::rpc::RpcClient;
use apex_sender_client::{serialize_transaction, Region};

let rpc = RpcClient::new(Region::Frankfurt, &api_key);
let wire = serialize_transaction(&tx)?;
let signature = rpc.send_transaction_binary(&wire, false, None).await?;
println!("accepted: {signature}");
```
