Documentation

Glaive Docs

Solana transaction sender API reference, regional endpoints, MEV protection, and the official Glaive tip account addresses required for submitted transactions.

Overview

Glaive is a high-performance Solana transaction sender. It maintains persistent connections to current and upcoming slot leaders, delivering your transactions directly into the validator processing pipeline.

The API is JSON-RPC compatible. You can use it as a drop-in replacement for your existing RPC endpoint when sending transactions.

Quickstart

Get a transaction submitted through Glaive in under a minute.

1. Get an API key

Contact the Glaive team to get an API key.

2. Send a transaction

curl "http://ams1.glaive.trade?key=YOUR_API_KEY" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sendTransaction",
    "params": [
      "<base64-encoded-tx>",
      { "encoding": "base64" }
    ]
  }'
const res = await fetch(
  `http://ams1.glaive.trade?key=${process.env.GLAIVE_KEY}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "sendTransaction",
      params: [
        "<base64-encoded-tx>",
        { encoding: "base64" },
      ],
    }),
  }
);

const { result: signature } = await res.json();
let client = reqwest::Client::new();
let url = format!("http://ams1.glaive.trade?key={}", api_key);

let res = client.post(&url)
    .json(&serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "sendTransaction",
        "params": [
            "<base64-encoded-tx>",
            { "encoding": "base64" }
        ]
    }))
    .send()
    .await?;

let body: serde_json::Value = res.json().await?;
let signature = &body["result"];
body := []byte(`{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sendTransaction",
    "params": [
        "<base64-encoded-tx>",
        { "encoding": "base64" }
    ]
}`)

url := fmt.Sprintf("http://ams1.glaive.trade?key=%s", apiKey)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
signature := result["result"]

Authentication

All requests require a key query parameter. Each API key has an individually configured rate limit (TPS).

http://ams1.glaive.trade?key=your-api-key-here

Keys are UUID v4 strings. Treat them like passwords. Do not commit them to source control or share them in client-side code.

Don't have a key yet? Contact us to get one.

Regions

Glaive runs in multiple regions. Use the endpoint closest to your infrastructure.

RegionEndpointStatus
Amsterdam 1ams1.glaive.tradeLive
Amsterdam 2ams2.glaive.tradeLive
Frankfurtfra.glaive.tradeLive
New York (Newark)ny.glaive.tradeLive

Endpoint

Glaive runs regional endpoints. Send requests to the nearest region:

POST http://ams1.glaive.trade?key=YOUR_API_KEY

The endpoint accepts standard Solana JSON-RPC format.

sendTransaction

Submit a signed transaction for delivery to current and upcoming leaders. Glaive offers four ways to submit transactions:

JSON-RPC

POST /

Submit a signed transaction using the standard Solana JSON-RPC format.

Request

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sendTransaction",
  "params": [
    "<base64-encoded-transaction>",
    {
      "encoding": "base64"
    }
  ]
}

Parameters

FieldTypeDescription
params[0]stringBase64-encoded signed transaction
encodingstringTransaction encoding: base64 or base58. Default: base64

Response

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": "<transaction-signature>"
}

Example

curl "http://ams1.glaive.trade?key=YOUR_API_KEY" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sendTransaction",
    "params": [
      "<base64-encoded-transaction>",
      { "encoding": "base64" }
    ]
  }'
// transaction = a signed VersionedTransaction
const serialized = transaction.serialize();
const base64Tx = Buffer.from(serialized).toString("base64");

const res = await fetch(
  `http://ams1.glaive.trade?key=${process.env.GLAIVE_KEY}`,
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "sendTransaction",
      params: [base64Tx, { encoding: "base64" }],
    }),
  }
);

const { result, error } = await res.json();
if (error) throw new Error(error.message);
console.log("Signature:", result);
// tx = a signed VersionedTransaction
let serialized = bincode::serialize(&tx)?;
let base64_tx = base64::engine::general_purpose::STANDARD.encode(&serialized);

let client = reqwest::Client::new();
let url = format!("http://ams1.glaive.trade?key={}", api_key);

let res = client.post(&url)
    .json(&serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "sendTransaction",
        "params": [base64_tx, { "encoding": "base64" }]
    }))
    .send()
    .await?;

let body: serde_json::Value = res.json().await?;
println!("Signature: {}", body["result"]);
// serializedTx = signed transaction bytes ([]byte)
base64Tx := base64.StdEncoding.EncodeToString(serializedTx)

body := []byte(`{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sendTransaction",
    "params": [
        "` + base64Tx + `",
        { "encoding": "base64" }
    ]
}`)

url := fmt.Sprintf("http://ams1.glaive.trade?key=%s", apiKey)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
if e, ok := result["error"]; ok {
    log.Fatalf("Error: %v", e)
}
fmt.Println("Signature:", result["result"])

Plain

POST /plain

A lightweight alternative to JSON-RPC. Post a base64-encoded transaction as the raw request body to /plain. No JSON wrapping needed.

Request

POST http://ams1.glaive.trade/plain?api-key=YOUR_API_KEY

The request body is the base64-encoded signed transaction as plain text.

Query Parameters

ParameterTypeDescription
api-keystringYour API key (required when rate limiting is enabled)
mev-protectstringSet to true to enable MEV protection

Response

{
  "result": "<transaction-signature>"
}

Error

Returns HTTP 400 with:

{
  "error": "<error-message>"
}

Example

curl "http://ams1.glaive.trade/plain?api-key=YOUR_API_KEY" \
  -X POST \
  -d '<base64-encoded-transaction>'
// transaction = a signed VersionedTransaction
const serialized = transaction.serialize();
const base64Tx = Buffer.from(serialized).toString("base64");

const res = await fetch(
  `http://ams1.glaive.trade/plain?api-key=${process.env.GLAIVE_KEY}`,
  {
    method: "POST",
    body: base64Tx,
  }
);

const { result, error } = await res.json();
if (error) throw new Error(error);
console.log("Signature:", result);
// tx = a signed VersionedTransaction
let serialized = bincode::serialize(&tx)?;
let base64_tx = base64::engine::general_purpose::STANDARD.encode(&serialized);

let client = reqwest::Client::new();
let url = format!("http://ams1.glaive.trade/plain?api-key={}", api_key);

let res = client.post(&url)
    .body(base64_tx)
    .send()
    .await?;

let body: serde_json::Value = res.json().await?;
println!("Signature: {}", body["result"]);
// serializedTx = signed transaction bytes ([]byte)
base64Tx := base64.StdEncoding.EncodeToString(serializedTx)

url := fmt.Sprintf("http://ams1.glaive.trade/plain?api-key=%s", apiKey)
resp, err := http.Post(url, "text/plain", strings.NewReader(base64Tx))
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
if e, ok := result["error"]; ok {
    log.Fatalf("Error: %v", e)
}
fmt.Println("Signature:", result["result"])

Binary

POST /binary

The lowest overhead option. Post the raw binary transaction bytes to /binary. No encoding, no JSON.

Request

POST http://ams1.glaive.trade/binary?api-key=YOUR_API_KEY

The request body is the raw serialized transaction bytes.

Query Parameters

ParameterTypeDescription
api-keystringYour API key (required when rate limiting is enabled)
mev-protectstringSet to true to enable MEV protection

Response

{
  "result": "<transaction-signature>"
}

Error

Returns HTTP 400 with:

{
  "error": "<error-message>"
}

Example

curl "http://ams1.glaive.trade/binary?api-key=YOUR_API_KEY" \
  -X POST \
  -H "Content-Type: application/octet-stream" \
  --data-binary "$SERIALIZED_TX_BYTES"
// transaction = a signed VersionedTransaction
const serialized = transaction.serialize();

const res = await fetch(
  `http://ams1.glaive.trade/binary?api-key=${process.env.GLAIVE_KEY}`,
  {
    method: "POST",
    body: serialized,
  }
);

const { result, error } = await res.json();
if (error) throw new Error(error);
console.log("Signature:", result);
// tx = a signed VersionedTransaction
let serialized = bincode::serialize(&tx)?;

let client = reqwest::Client::new();
let url = format!("http://ams1.glaive.trade/binary?api-key={}", api_key);

let res = client.post(&url)
    .body(serialized)
    .send()
    .await?;

let body: serde_json::Value = res.json().await?;
println!("Signature: {}", body["result"]);
// serializedTx = signed transaction bytes ([]byte)
url := fmt.Sprintf("http://ams1.glaive.trade/binary?api-key=%s", apiKey)
resp, err := http.Post(url, "application/octet-stream", bytes.NewReader(serializedTx))
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
if e, ok := result["error"]; ok {
    log.Fatalf("Error: %v", e)
}
fmt.Println("Signature:", result["result"])

QUIC

UDP/4000

Maintain a single persistent QUIC connection to a Glaive region and stream transactions with no per-send HTTP overhead. This is the lowest-latency path Glaive offers, intended for HFT and latency sensitive trading workloads.

Use this if you are colocated with a region, if every millisecond counts, or if you were already pooling HTTP connections to avoid TCP and TLS setup on each send.

Endpoint

udp://<region>.glaive.trade:4000

QUIC listens on UDP port 4000 in every region.

Protocol

  • Transport: QUIC over UDP.
  • TLS: self-signed Solana style X.509 via solana-tls-utils::new_dummy_x509_certificate. The server does not validate the client certificate and the client should skip server verification.
  • ALPN: solana-tpu.
  • SNI: glaive-intake.
  • Keep alive: 10s recommended. Idle timeout is 30s server side.

Authentication

Immediately after the handshake, open one unidirectional stream and write a 17 byte auth frame, then finish the stream. Auth applies for the lifetime of the connection.

OffsetSizeField
016API key UUID as a big-endian u128 (dashes stripped, hex decoded)
161Flags. Bit 0 enables MEV-protect routing for every transaction on this connection.

Sending Transactions

For each transaction, open a new unidirectional stream, write the raw serialized transaction bytes, and finish the stream. One stream carries exactly one transaction. Maximum size is 1232 bytes.

Example

use std::net::{SocketAddr, ToSocketAddrs};
use std::sync::Arc;
use std::time::Duration;

use anyhow::Context as _;
use arc_swap::ArcSwap;
use quinn::crypto::rustls::QuicClientConfig;
use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig};
use solana_keypair::Keypair;
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
use tokio::sync::Mutex;

const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const GLAIVE_QUIC_SNI: &str = "glaive-intake";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
const MEV_PROTECT_BIT: u8 = 1 << 0;

pub struct GlaiveQuicClient {
    endpoint: Endpoint,
    client_config: ClientConfig,
    addr: SocketAddr,
    auth_frame: [u8; 17],
    connection: ArcSwap<Connection>,
    reconnect: Mutex<()>,
}

impl GlaiveQuicClient {
    pub async fn connect(
        endpoint_addr: &str,
        api_key_uuid: &str,
        mev_protect: bool,
    ) -> anyhow::Result<Self> {
        let api_key = parse_uuid_u128(api_key_uuid).context("invalid API key UUID")?;
        let client_config = build_client_config()?;

        let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
        endpoint.set_default_client_config(client_config.clone());

        let addr = endpoint_addr
            .to_socket_addrs()?
            .next()
            .ok_or_else(|| anyhow::anyhow!("could not resolve {}", endpoint_addr))?;

        let connection = endpoint.connect(addr, GLAIVE_QUIC_SNI)?.await?;

        let mut auth_frame = [0u8; 17];
        auth_frame[..16].copy_from_slice(&api_key.to_be_bytes());
        auth_frame[16] = if mev_protect { MEV_PROTECT_BIT } else { 0 };

        send_auth_frame(&connection, &auth_frame).await?;

        Ok(Self {
            endpoint,
            client_config,
            addr,
            auth_frame,
            connection: ArcSwap::from_pointee(connection),
            reconnect: Mutex::new(()),
        })
    }

    pub async fn send_transaction(&self, tx_bytes: &[u8]) -> anyhow::Result<()> {
        let conn = self.connection.load_full();
        if write_tx(&conn, tx_bytes).await.is_ok() {
            return Ok(());
        }
        self.reconnect(&conn).await?;
        let conn = self.connection.load_full();
        write_tx(&conn, tx_bytes).await
    }

    async fn reconnect(&self, stale: &Arc<Connection>) -> anyhow::Result<()> {
        let _guard = self.reconnect.lock().await;
        if !Arc::ptr_eq(&self.connection.load_full(), stale) {
            return Ok(());
        }
        let connection = self
            .endpoint
            .connect_with(self.client_config.clone(), self.addr, GLAIVE_QUIC_SNI)?
            .await?;
        send_auth_frame(&connection, &self.auth_frame).await?;
        self.connection.store(Arc::new(connection));
        Ok(())
    }
}

async fn send_auth_frame(conn: &Connection, frame: &[u8; 17]) -> anyhow::Result<()> {
    let mut send = conn.open_uni().await?;
    send.write_all(frame).await?;
    send.finish()?;
    Ok(())
}

async fn write_tx(conn: &Connection, tx_bytes: &[u8]) -> anyhow::Result<()> {
    let mut send = conn.open_uni().await?;
    send.write_all(tx_bytes).await?;
    send.finish()?;
    Ok(())
}

fn build_client_config() -> anyhow::Result<ClientConfig> {
    let keypair = Keypair::new();
    let (cert, key) = new_dummy_x509_certificate(&keypair);

    let mut crypto = rustls::ClientConfig::builder()
        .dangerous()
        .with_custom_certificate_verifier(SkipServerVerification::new())
        .with_client_auth_cert(vec![cert], key)
        .context("failed to configure client certificate")?;
    crypto.alpn_protocols = vec![ALPN_TPU_PROTOCOL_ID.to_vec()];

    let quic_crypto = QuicClientConfig::try_from(crypto)?;
    let mut client_config = ClientConfig::new(Arc::new(quic_crypto));

    let mut transport = TransportConfig::default();
    transport
        .keep_alive_interval(Some(KEEP_ALIVE_INTERVAL))
        .max_idle_timeout(Some(IdleTimeout::try_from(MAX_IDLE_TIMEOUT)?));
    client_config.transport_config(Arc::new(transport));

    Ok(client_config)
}

fn parse_uuid_u128(s: &str) -> anyhow::Result<u128> {
    let b = s.as_bytes();
    if b.len() != 36 || b[8] != b'-' || b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
        anyhow::bail!("expected UUID like xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
    }
    let mut out: u128 = 0;
    for (i, &c) in b.iter().enumerate() {
        if i == 8 || i == 13 || i == 18 || i == 23 { continue; }
        let nibble = match c {
            b'0'..=b'9' => c - b'0',
            b'a'..=b'f' => c - b'a' + 10,
            b'A'..=b'F' => c - b'A' + 10,
            _ => anyhow::bail!("invalid hex character in UUID"),
        };
        out = (out << 4) | (nibble as u128);
    }
    Ok(out)
}

Usage

let client = GlaiveQuicClient::connect(
    "ams1.glaive.trade:4000",
    "YOUR_API_KEY_UUID",
    false, // mev_protect
).await?;

// tx = a signed VersionedTransaction
let serialized = bincode::serialize(&tx)?;
client.send_transaction(&serialized).await?;

MEV Protection

Glaive can exclude validators known to sandwich transactions from your transaction's delivery path. This is disabled by default. To opt in, add mev_protect=true to the query string:

POST http://ams1.glaive.trade?key=YOUR_API_KEY&mev_protect=true

When enabled, Glaive will skip any leader that appears on its sandwiching validator blacklist. The blacklist is maintained by the Glaive team and updated as new sandwich activity is detected.

Without mev_protect=true, transactions are sent to all upcoming leaders regardless of known sandwich behavior.

For the /plain and /binary endpoints, use mev-protect=true instead.

Minimum Tip

All transactions sent through Glaive must include a transfer of at least 0.001 SOL (1,000,000 lamports) to one of the Glaive tip accounts. Transactions without a valid tip will be rejected with a -32000 JSON-RPC error.

Tip Accounts

Your transaction must include a SOL transfer to any one of these Glaive tip addresses. The canonical list is also available at Glaive tip accounts.

  • GLaiv4GMRYQmthatDS98uQT4HoucgxWT8NeJz6oSwxeU
  • GLaivL5uPrDpvd1wTtvat38KGqb5WLhEdqQfnmNd3oNr
  • GLaivinAWh21NaJMhtExtD5G2gZs1xnvaYVZmwqobWZL
  • GLaivJSUL71FcocYa8tks5vpVyYzvaDMHtyrzfQF2ABr
  • GLaivRU6eDKrta3p3psFAWPEFLzCjeMHGpPUuQqTjtyv
  • GLaivq5dU8qHayz9Qf13LjPfVy3SmUhbmickfGiZdmfh
import { SystemProgram, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";

const GLAIVE_TIP_ACCOUNTS = [
  "GLaiv4GMRYQmthatDS98uQT4HoucgxWT8NeJz6oSwxeU",
  "GLaivL5uPrDpvd1wTtvat38KGqb5WLhEdqQfnmNd3oNr",
  "GLaivinAWh21NaJMhtExtD5G2gZs1xnvaYVZmwqobWZL",
  "GLaivJSUL71FcocYa8tks5vpVyYzvaDMHtyrzfQF2ABr",
  "GLaivRU6eDKrta3p3psFAWPEFLzCjeMHGpPUuQqTjtyv",
  "GLaivq5dU8qHayz9Qf13LjPfVy3SmUhbmickfGiZdmfh",
];

const TIP_LAMPORTS = 0.001 * LAMPORTS_PER_SOL; // 1_000_000
const tipAccount = GLAIVE_TIP_ACCOUNTS[Math.floor(Math.random() * GLAIVE_TIP_ACCOUNTS.length)];

tx.add(
  SystemProgram.transfer({
    fromPubkey: payer.publicKey,
    toPubkey: new PublicKey(tipAccount),
    lamports: TIP_LAMPORTS,
  })
);
use solana_sdk::{system_instruction, pubkey::Pubkey};
use rand::seq::SliceRandom;
use std::str::FromStr;

const TIP_LAMPORTS: u64 = 1_000_000; // 0.001 SOL

const GLAIVE_TIP_ACCOUNTS: &[&str] = &[
    "GLaiv4GMRYQmthatDS98uQT4HoucgxWT8NeJz6oSwxeU",
    "GLaivL5uPrDpvd1wTtvat38KGqb5WLhEdqQfnmNd3oNr",
    "GLaivinAWh21NaJMhtExtD5G2gZs1xnvaYVZmwqobWZL",
    "GLaivJSUL71FcocYa8tks5vpVyYzvaDMHtyrzfQF2ABr",
    "GLaivRU6eDKrta3p3psFAWPEFLzCjeMHGpPUuQqTjtyv",
    "GLaivq5dU8qHayz9Qf13LjPfVy3SmUhbmickfGiZdmfh",
];

let tip_account = GLAIVE_TIP_ACCOUNTS
    .choose(&mut rand::thread_rng())
    .unwrap();
let tip_pubkey = Pubkey::from_str(tip_account).unwrap();

let tip_ix = system_instruction::transfer(
    &payer.pubkey(),
    &tip_pubkey,
    TIP_LAMPORTS,
);
tx.add(tip_ix);
import (
    "math/rand"
    "github.com/gagliardetto/solana-go"
    "github.com/gagliardetto/solana-go/programs/system"
)

const TipLamports = 1_000_000 // 0.001 SOL

var GlaiveTipAccounts = []solana.PublicKey{
    solana.MustPublicKeyFromBase58("GLaiv4GMRYQmthatDS98uQT4HoucgxWT8NeJz6oSwxeU"),
    solana.MustPublicKeyFromBase58("GLaivL5uPrDpvd1wTtvat38KGqb5WLhEdqQfnmNd3oNr"),
    solana.MustPublicKeyFromBase58("GLaivinAWh21NaJMhtExtD5G2gZs1xnvaYVZmwqobWZL"),
    solana.MustPublicKeyFromBase58("GLaivJSUL71FcocYa8tks5vpVyYzvaDMHtyrzfQF2ABr"),
    solana.MustPublicKeyFromBase58("GLaivRU6eDKrta3p3psFAWPEFLzCjeMHGpPUuQqTjtyv"),
    solana.MustPublicKeyFromBase58("GLaivq5dU8qHayz9Qf13LjPfVy3SmUhbmickfGiZdmfh"),
}

tipAccount := GlaiveTipAccounts[rand.Intn(len(GlaiveTipAccounts))]

tipIx := system.NewTransferInstruction(
    TipLamports,
    payer.PublicKey(),
    tipAccount,
).Build()

Keep Alive

If you maintain persistent HTTP connections to Glaive (recommended for lowest latency), send periodic requests to the /health endpoint to keep them alive.

curl https://<region>.glaive.trade/health

The endpoint returns 200 OK with no authentication required. A ping every 15 to 30 seconds is enough to prevent idle connection timeouts.

Rate Limits

Each API key has a configured transactions-per-second (TPS) limit. When exceeded, requests return 429 Too Many Requests.

Error Codes

HTTP Status Codes

HTTP StatusMeaning
200Request processed (check JSON-RPC response for result or error)
401Missing or invalid API key
429Rate limit exceeded

JSON-RPC Error Codes

Validation and processing errors return HTTP 200 with a JSON-RPC error object in the response body.

CodeMeaning
-32600Invalid request (malformed JSON-RPC)
-32601Method not found (only sendTransaction is supported)
-32000Transaction rejected (invalid encoding, missing tip, etc.)

FAQ

Does Glaive replace my RPC provider?

Only for sendTransaction. You still need a standard RPC for reading state, fetching blockhashes, and other queries.

Which encoding formats are supported?

Base64 (default) and base58. We recommend base64 for smaller payload size.

What happens if the leader rotates mid-send?

Glaive fans out to multiple upcoming leaders simultaneously. Leader rotation during delivery is expected and handled automatically.

Is there a testnet endpoint?

Not currently. Glaive targets mainnet-beta only.

Contact

To get an API key or ask questions, reach out to us directly.