AllenHark Relay — Client Integration Guide

Introduction

AllenHark Relay is an ultra-low-latency Solana transaction relay designed for traders, market makers, and automated systems requiring sub-millisecond dispatch.

Your client submits a pre-signed Solana transaction to AllenHark Relay, and we immediately deliver it using a highly optimized, leader-aware pipeline engineered for speed — with stake-weighted priority delivery for maximum transaction inclusion rates.

Broadcast Pipeline

Every transaction is broadcast simultaneously through 5 channels:

  1. Stake-Weighted QUIC (Primary) — Priority delivery across multiple global endpoints
  2. QUIC TPU — Direct to current + next 48 Solana leaders via QUIC
  3. UDP TPU — Fire-and-forget UDP to leaders
  4. RPC Fan-out — Parallel sendTransaction to multiple RPC endpoints
  5. ROC Relayers — Jito Block Engine and custom relayers

Submission Modes

We provide three submission modes:

A) QUIC (Recommended — 0.1ms possible)

Persistent, connection-oriented, extremely low overhead.

B) WebSocket (wss://) — persistent, browser-native

Open one socket per region, then stream transactions or atomic bundles for as long as you like. One TLS handshake at connect, none afterwards. Works from browsers and every modern language without a custom client.

C) Standard HTTPS

Simplest integration, but typically 15–25ms latency.

All three modes accept both single transactions and atomic Jito bundles — up to 5 transactions that land together or not at all, submitted straight to the Jito Block Engine.


Multi-Region Endpoints

AllenHark Relay operates across multiple regions. Each region runs an independent relay instance with full 5-channel broadcast pipeline.

QUIC Endpoints (Recommended)

QUIC connections use IP addresses directly for lowest latency:

RegionQUIC AddressIP
Frankfurt (Primary)84.32.223.83:443384.32.223.83
Amsterdam84.32.104.38:443384.32.104.38
New York66.94.98.217:443366.94.98.217
Tokyo88.216.188.28:443388.216.188.28

HTTPS Endpoints

HTTP submission uses domain names:

RegionHTTPS EndpointDomain
Frankfurt (Primary)https://fra.relay.allenhark.com/v1/sendTxfra.relay.allenhark.com
Amsterdamhttps://ams.relay.allenhark.com/v1/sendTxams.relay.allenhark.com
New Yorkhttps://ny.relay.allenhark.com/v1/sendTxny.relay.allenhark.com
Tokyohttps://tyo.relay.allenhark.com/v1/sendTxtyo.relay.allenhark.com

WebSocket Endpoints

Persistent wss:// streams. Two paths per region — one for single transactions and one for atomic bundles:

RegionSingle-tx streamBundle stream
Frankfurt (Primary)wss://fra.relay.allenhark.com/v1/stream/txwss://fra.relay.allenhark.com/v1/stream/bundle
Amsterdamwss://ams.relay.allenhark.com/v1/stream/txwss://ams.relay.allenhark.com/v1/stream/bundle
New Yorkwss://ny.relay.allenhark.com/v1/stream/txwss://ny.relay.allenhark.com/v1/stream/bundle
Tokyowss://tyo.relay.allenhark.com/v1/stream/txwss://tyo.relay.allenhark.com/v1/stream/bundle

[!TIP] relay.allenhark.com is an alias for fra.relay.allenhark.com (Frankfurt). You can use either for HTTPS.

[!IMPORTANT] QUIC Protocol (Recommended): Always use the IP address directly for QUIC connections. Works efficiently from any location worldwide. All stake-weighted endpoints are used simultaneously regardless of your region.

HTTPS/REST: Uses domain names. Best when co-located with the relay region. For cross-region, use QUIC.


API Authentication

API keys are optional. You can submit transactions without an API key. Providing one enables rate tracking, transaction history, and dedicated relay features.

Without API Key

1const response = await fetch("https://relay.allenhark.com/v1/sendTx", {
2  method: "POST",
3  headers: { "Content-Type": "application/json" },
4  body: JSON.stringify({
5    tx: "BASE64_ENCODED_TRANSACTION",
6    simulate: false
7  })
8});

With API Key (Header)

1const response = await fetch("https://relay.allenhark.com/v1/sendTx", {
2  method: "POST",
3  headers: {
4    "Content-Type": "application/json",
5    "x-api-key": "YOUR_API_KEY"
6  },
7  body: JSON.stringify({
8    tx: "BASE64_ENCODED_TRANSACTION",
9    simulate: false
10  })
11});

With API Key (Query Parameter)

1const response = await fetch("https://relay.allenhark.com/v1/sendTx?api-key=YOUR_API_KEY", {
2  method: "POST",
3  headers: { "Content-Type": "application/json" },
4  body: JSON.stringify({
5    tx: "BASE64_ENCODED_TRANSACTION",
6    simulate: false
7  })
8});

Rate Limits

  • Without API key: no rate limiting (subject to change)
  • With API key: 100 requests per second (default)
  • Custom Limits: Contact us for higher rate limits based on your trading volume
  • Rate limits are applied per API key, not per IP address

Tip Wallets

To ensure priority processing, every transaction must include a tip to one of the AllenHark tip wallets. This economic model ensures fairness and prevents spam.

Available Tip Wallets (Select Random):

1const TIP_WALLETS = [
2  "hark1zxc5Rz3K8Kquz79WPWFEgNCFeJnsMJ16f22uNP",
3  "harkm2BTWxZuszoNpZnfe84jRbQTg6KGHaQBmWzDGQQ",
4  "hark4CwtTnN2y9FaxjcFBAJdJqQrpouu5pgEixfqdEz",
5  "harkoJfnM6dxrJydx5eVmDVwAgwC94KbhuxF69UbXwP",
6  "hark6hUDUTekc1DGxWdJcuyDZwf6pJdCxd4SXAVtta6",
7  "harkoTvFpKSrEQduYrNHXCurARVT19Ud3BnFhVxabos",
8  "harkEpXoJv5qVzHaN7HSuUAd6PHjyMcFMcDYBMDJCEQ",
9  "harkyXDdZSoJGyCxa24t2QXx1poPyp8YfghbtpzGSzK",
10  "harkR2YJ4Dpt4UDJTcBirjnSPBhNpQFcoFkNpCkVqNk",
11  "harkRBygM8pHYe4K8eBjfxyEX19oJn3LepFjvNbLbyi",
12  "harkYFxB6DuUFNwDLvA5CQ66KpfRvFgUoVypMagNcmd",
13];
14
15// Select a random tip wallet for each transaction
16const tipWallet = TIP_WALLETS[Math.floor(Math.random() * TIP_WALLETS.length)];
1// Rust example
2const TIP_WALLETS: [&str; 11] = [
3    "hark1zxc5Rz3K8Kquz79WPWFEgNCFeJnsMJ16f22uNP",
4    "harkm2BTWxZuszoNpZnfe84jRbQTg6KGHaQBmWzDGQQ",
5    "hark4CwtTnN2y9FaxjcFBAJdJqQrpouu5pgEixfqdEz",
6    "harkoJfnM6dxrJydx5eVmDVwAgwC94KbhuxF69UbXwP",
7    "hark6hUDUTekc1DGxWdJcuyDZwf6pJdCxd4SXAVtta6",
8    "harkoTvFpKSrEQduYrNHXCurARVT19Ud3BnFhVxabos",
9    "harkEpXoJv5qVzHaN7HSuUAd6PHjyMcFMcDYBMDJCEQ",
10    "harkyXDdZSoJGyCxa24t2QXx1poPyp8YfghbtpzGSzK",
11    "harkR2YJ4Dpt4UDJTcBirjnSPBhNpQFcoFkNpCkVqNk",
12    "harkRBygM8pHYe4K8eBjfxyEX19oJn3LepFjvNbLbyi",
13    "harkYFxB6DuUFNwDLvA5CQ66KpfRvFgUoVypMagNcmd",
14];
15
16// Select a random tip wallet
17let tip_wallet = TIP_WALLETS[rand::random::<usize>() % TIP_WALLETS.len()];

Minimum Tip Amount

AllenHark Relay requires a minimum tip for each transaction:

  • Minimum: 0.001 SOL (1,000,000 lamports)
  • Recommended during congestion: 0.002–0.005 SOL

Transactions without a valid tip to an AllenHark wallet will be rejected with error code tip_missing.

[!NOTE] Jito tips do not count toward the relay tip — a transfer to a Jito tip account and a transfer to an AllenHark wallet are two separate requirements. Single transactions only need the AllenHark tip. Atomic bundles need both: the AllenHark tip (unless you're a dedicated customer) and a Jito tip, because bundles are executed by the Jito Block Engine.

Dedicated Relay (Tip-Free)

Dedicated relay customers with whitelisted signer pubkeys can skip tip validation entirely. Transactions signed by whitelisted pubkeys are broadcast immediately without requiring tips.

To set up dedicated relay:

  1. Purchase a Dedicated Relay subscription on the console
  2. Verify your wallet pubkey(s) by signing a challenge message
  3. Transactions from verified pubkeys are relayed tip-free

Transaction Support

Supported Transaction Types

  • Legacy (V0) transactions — fully supported
  • Versioned transactions — supported with restrictions

Address Lookup Tables (ALT)

Transactions using Address Lookup Tables are rejected for non-dedicated customers because the relay cannot verify tip destinations without resolving the ALT on-chain. Dedicated relay customers with whitelisted pubkeys are exempt from this restriction.


Sending via QUIC (Recommended)

QUIC provides ultra-low latency through:

  • Persistent Connections: Connection established once, reused for all transactions
  • Connection Multiplexing: Multiple streams over a single connection
  • No Head-of-Line Blocking: Independent stream processing
  • Built-in TLS 1.3: Encrypted by default
  • 50-80% Latency Reduction compared to HTTP/2

QUIC Endpoint

Use the IP address directly for QUIC connections:

84.32.223.83:4433 # Frankfurt (Primary) 84.32.104.38:4433 # Amsterdam 66.94.98.217:4433 # New York 88.216.188.28:4433 # Tokyo

TLS Configuration

The QUIC server uses standard TLS 1.3. Clients should:

  • Skip server certificate verification (or use system root certs)
  • No ALPN required — do not set any ALPN protocols
  • No client certificate required — anonymous connections accepted
  • API key is optional — sent via the stream header, not TLS
1// Quinn / rustls 0.23 — minimal QUIC client config
2let crypto = rustls::ClientConfig::builder()
3    .dangerous()
4    .with_custom_certificate_verifier(SkipServerVerification::new())
5    .with_no_client_auth();
6
7let client_config = quinn::ClientConfig::new(Arc::new(
8    QuicClientConfig::try_from(crypto).unwrap()
9));

Stream Protocol

Each transaction uses a bidirectional QUIC stream:

  1. Client sends API key header (optional — send \n if no key):

    api-key: YOUR_API_KEY\n
  2. Client sends JSON payload:

    1{"tx": "BASE64_TRANSACTION", "simulate": false}
  3. Client closes write side (signals end of request)

  4. Server responds:

    1{
    2  "status": "accepted",
    3  "request_id": "550e8400-e29b-41d4-a716-446655440000",
    4  "signature": "5wH...txSig"
    5}

Bundle frames use the same stream protocol — send txs (an array) instead of tx in step 2:

1{"txs": ["BASE64_TX_1", "BASE64_TX_2"], "id": "b1"}

The response carries the Jito bundle id instead of a single signature:

1{"status": "accepted", "bundle_id": "jito-uuid", "signatures": ["sig1", "sig2"], "id": "b1"}

See Atomic Bundles (Jito) for tip requirements and landing semantics.

Rust Example (Quinn 0.11 + rustls 0.23)

1use quinn::{Endpoint, crypto::rustls::QuicClientConfig};
2use std::sync::Arc;
3
4// Skip server cert verification (relay uses Let's Encrypt)
5struct SkipVerify;
6impl rustls::client::danger::ServerCertVerifier for SkipVerify {
7    fn verify_server_cert(&self, _: &rustls::pki_types::CertificateDer, _: &[rustls::pki_types::CertificateDer], _: &rustls::pki_types::ServerName, _: &[u8], _: rustls::pki_types::UnixTime) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> { Ok(rustls::client::danger::ServerCertVerified::assertion()) }
8    fn verify_tls12_signature(&self, _: &[u8], _: &rustls::pki_types::CertificateDer, _: &rustls::DigitallySignedStruct) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) }
9    fn verify_tls13_signature(&self, _: &[u8], _: &rustls::pki_types::CertificateDer, _: &rustls::DigitallySignedStruct) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) }
10    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> { rustls::crypto::ring::default_provider().signature_verification_algorithms.supported_schemes() }
11}
12
13#[tokio::main]
14async fn main() -> Result<(), Box<dyn std::error::Error>> {
15    // Configure QUIC client — no ALPN, no client cert, skip server verify
16    let crypto = rustls::ClientConfig::builder()
17        .dangerous()
18        .with_custom_certificate_verifier(Arc::new(SkipVerify))
19        .with_no_client_auth();
20
21    let client_config = quinn::ClientConfig::new(Arc::new(
22        QuicClientConfig::try_from(crypto)?
23    ));
24
25    let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
26    endpoint.set_default_client_config(client_config);
27
28    // Connect to relay via IP (connection persists — reuse for all transactions)
29    let conn = endpoint
30        .connect("84.32.223.83:4433".parse()?, "relay.allenhark.com")?
31        .await?;
32
33    // Open bidirectional stream per transaction
34    let (mut send, mut recv) = conn.open_bi().await?;
35
36    // Send header (API key optional — just send newline if none)
37    send.write_all(b"\n").await?;
38
39    // Send transaction
40    let payload = serde_json::json!({
41        "tx": "BASE64_TRANSACTION",
42        "simulate": false
43    });
44    send.write_all(&serde_json::to_vec(&payload)?).await?;
45    send.finish()?;
46
47    // Read response
48    let response = recv.read_to_end(4096).await?;
49    let result: serde_json::Value = serde_json::from_slice(&response)?;
50    println!("Response: {}", result);
51    
52    Ok(())
53}

Python Example (aioquic)

1import asyncio
2import json
3from aioquic.asyncio import connect
4from aioquic.quic.configuration import QuicConfiguration
5
6async def send_transaction(api_key: str, tx_base64: str):
7    configuration = QuicConfiguration(is_client=True)
8    configuration.verify_mode = True
9    
10    async with connect(
11        "84.32.223.83",  # Frankfurt IP
12        4433,
13        configuration=configuration,
14    ) as client:
15        # Open stream
16        stream_id = client._quic.get_next_available_stream_id()
17        
18        # Send API key
19        client._quic.send_stream_data(
20            stream_id,
21            f"api-key: {api_key}\n".encode()
22        )
23        
24        # Send transaction
25        payload = json.dumps({
26            "tx": tx_base64,
27            "simulate": False
28        })
29        client._quic.send_stream_data(stream_id, payload.encode(), end_stream=True)
30        
31        # Read response
32        response_data = await client._quic.receive_stream_data(stream_id)
33        response = json.loads(response_data.decode())
34        print("Response:", response)
35
36asyncio.run(send_transaction("YOUR_API_KEY", "BASE64_TX"))

Node.js Note

Node.js QUIC support is experimental. We recommend using Rust for QUIC integration or the HTTPS endpoint for Node.js applications.


Sending via HTTPS

A simpler integration path using standard REST API. API keys are optional — if provided, they enable rate tracking and dedicated relay features.

Endpoints

RegionSingle transactionAtomic bundle
PrimaryPOST https://relay.allenhark.com/v1/sendTxPOST https://relay.allenhark.com/v1/sendBundle
FrankfurtPOST https://fra.relay.allenhark.com/v1/sendTxPOST https://fra.relay.allenhark.com/v1/sendBundle
AmsterdamPOST https://ams.relay.allenhark.com/v1/sendTxPOST https://ams.relay.allenhark.com/v1/sendBundle
New YorkPOST https://ny.relay.allenhark.com/v1/sendTxPOST https://ny.relay.allenhark.com/v1/sendBundle
TokyoPOST https://tyo.relay.allenhark.com/v1/sendTxPOST https://tyo.relay.allenhark.com/v1/sendBundle

Bundle request/response shapes are covered in Atomic Bundles (Jito).

Keep-Alive

Use the /keepalive endpoint to maintain your HTTP connection and monitor server time:

1curl https://relay.allenhark.com/keepalive
1{
2  "status": "alive",
3  "server_time": "2026-05-02T12:00:00.000Z",
4  "region": "fra",
5  "slot": 312345678
6}

Poll /keepalive every 5–10 seconds to keep your connection warm and avoid repeated TLS handshakes.

Request Format

1{
2  "tx": "BASE64_ENCODED_TRANSACTION",
3  "simulate": false
4}

[!IMPORTANT] The simulate parameter must be false. Simulation requests are rejected to prevent abuse.

Node.js Example (fetch)

1// Without API key
2const response = await fetch("https://relay.allenhark.com/v1/sendTx", {
3  method: "POST",
4  headers: { "Content-Type": "application/json" },
5  body: JSON.stringify({
6    tx: "BASE64_ENCODED_TRANSACTION",
7    simulate: false
8  })
9});
10
11const data = await response.json();
12console.log("Response:", data);
13
14// With API key via query param
15const response2 = await fetch(
16  "https://relay.allenhark.com/v1/sendTx?api-key=YOUR_API_KEY",
17  {
18    method: "POST",
19    headers: { "Content-Type": "application/json" },
20    body: JSON.stringify({ tx: "BASE64_TX", simulate: false })
21  }
22);

Rust Example (reqwest)

1use reqwest::Client;
2use serde_json::json;
3
4#[tokio::main]
5async fn main() -> Result<(), Box<dyn std::error::Error>> {
6    let client = Client::new();
7    
8    let res = client.post("https://relay.allenhark.com/v1/sendTx")
9        .header("x-api-key", "YOUR_API_KEY")
10        .json(&json!({
11            "tx": "BASE64_ENCODED_TRANSACTION",
12            "simulate": false
13        }))
14        .send()
15        .await?;
16
17    let response_text = res.text().await?;
18    println!("Response: {}", response_text);
19    Ok(())
20}

Sending via WebSocket

Persistent wss:// connection — open once, stream as many transactions or bundles as you need. The TLS handshake is paid once at connect time and amortized across every subsequent message. Browser-native, language-agnostic, identical validation and tip rules to HTTPS.

Endpoints

StreamURL pattern
Single transactionswss://{region}.relay.allenhark.com/v1/stream/tx
Atomic bundles (≤ 5 txs)wss://{region}.relay.allenhark.com/v1/stream/bundle

{region} is fra, ams, ny, or tyo. relay.allenhark.com is an alias for Frankfurt.

Authentication

Same as HTTPS — auth happens once at the upgrade request, not per message. Choose either form:

1// Header (preferred for non-browser clients)
2new WebSocket("wss://relay.allenhark.com/v1/stream/tx", undefined, {
3  headers: { "x-api-key": "YOUR_API_KEY" }
4});
5
6// Query parameter (works from browsers, where headers can't be set on WS)
7new WebSocket("wss://relay.allenhark.com/v1/stream/tx?api-key=YOUR_API_KEY");

API keys are optional. Without one, public submissions still work; with one, you get rate tracking and dedicated relay features.

Single-Transaction Stream

Each frame you send is one JSON object:

1{ "id": "client-correlation-id", "tx": "BASE64_ENCODED_TRANSACTION" }

The server replies with a matching frame:

1{ "id": "client-correlation-id", "status": "accepted", "request_id": "uuid", "signature": "5xy…" }

On rejection:

1{ "id": "client-correlation-id", "status": "rejected", "error": "Tip 100000 below threshold 1000000" }

The id field is yours — it's echoed back verbatim so you can match each reply to the request that produced it. Replies may arrive out of order; correlate by id.

Bundle Stream

Atomic groups of up to 5 transactions that land together or not at all:

1{ "id": "b1", "txs": ["BASE64_TX_1", "BASE64_TX_2", "BASE64_TX_3"] }

Server reply:

1{ "id": "b1", "status": "accepted", "bundle_id": "jito-uuid", "signatures": ["sig1", "sig2", "sig3"] }

bundle_id is the Jito Block Engine's bundle id — you can track the bundle with it directly. Tip convention: any single transaction paying the standard 0.001 SOL AllenHark tip covers the whole group, and the bundle must additionally include a Jito tip. Bundles are submitted only through the Jito Block Engine's atomic path — never split across TPU/RPC channels — so atomicity is preserved end-to-end.

Pipelined Sends

You don't have to wait for a reply before sending the next message. Fire as many frames as your rate limit allows; replies stream back asynchronously, identified by id.

1for (const tx of myTxs) {
2  ws.send(JSON.stringify({ id: tx.localId, tx: tx.b64 }));
3}
4// Replies arrive out of order — match by id.

A rate-limit rejection on one frame doesn't close the connection — the next frame goes through normally.

Browser Example

1const ws = new WebSocket(
2  "wss://fra.relay.allenhark.com/v1/stream/tx?api-key=YOUR_API_KEY"
3);
4
5ws.onopen = () => {
6  ws.send(JSON.stringify({ id: "1", tx: BASE64_TX }));
7};
8
9ws.onmessage = (e) => {
10  const r = JSON.parse(e.data);
11  if (r.status === "accepted") {
12    console.log("landed", r.signature);
13  } else {
14    console.error("rejected", r.error);
15  }
16};

Node.js Example (ws)

1import WebSocket from "ws";
2
3const ws = new WebSocket("wss://fra.relay.allenhark.com/v1/stream/tx", {
4  headers: { "x-api-key": "YOUR_API_KEY" },
5});
6
7ws.on("open", () => {
8  ws.send(JSON.stringify({ id: "1", tx: BASE64_TX }));
9});
10
11ws.on("message", (data) => {
12  const r = JSON.parse(data.toString());
13  console.log(r);
14});

Python Example (websockets)

1import asyncio, json, websockets
2
3async def submit(tx_b64_list):
4    async with websockets.connect(
5        "wss://fra.relay.allenhark.com/v1/stream/tx",
6        additional_headers={"x-api-key": "YOUR_API_KEY"},
7    ) as ws:
8        # Pipeline: fire all txs, collect replies as they arrive
9        for i, tx in enumerate(tx_b64_list):
10            await ws.send(json.dumps({"id": str(i), "tx": tx}))
11        for _ in tx_b64_list:
12            print(json.loads(await ws.recv()))

Rust Example (tokio-tungstenite)

1use futures_util::{SinkExt, StreamExt};
2use tokio_tungstenite::{connect_async, tungstenite::Message};
3
4#[tokio::main]
5async fn main() -> anyhow::Result<()> {
6    let url = "wss://fra.relay.allenhark.com/v1/stream/tx?api-key=YOUR_API_KEY";
7    let (mut ws, _) = connect_async(url).await?;
8
9    let frame = serde_json::json!({ "id": "1", "tx": tx_base64 });
10    ws.send(Message::Text(frame.to_string())).await?;
11
12    while let Some(msg) = ws.next().await {
13        if let Message::Text(text) = msg? {
14            println!("{}", text);
15            break;
16        }
17    }
18    Ok(())
19}

Connection Lifecycle

  • Idle behavior: streams stay alive aggressively — no idle timeout you need to design around. A connection opened at process boot will stay hot through quiet periods.
  • Reconnect: on transient drops (network blip, server restart), reconnect immediately and resume sending. There's no session state to recover — each frame is independent.
  • Close: send a close frame when you're done; the server will flush any in-flight replies and finish the close handshake.

[!IMPORTANT] Tip and validation rules are identical to HTTPS and QUIC — minimum 0.001 SOL to a registered tip wallet, no transactions using Address Lookup Tables (ALT), max ~1.6KB per transaction. Bundles inherit the same threshold (one tx paying the AllenHark tip covers the whole bundle) and additionally require a Jito tip.


Atomic Bundles (Jito)

Bundles are ordered groups of up to 5 transactions that execute atomically — all of them land in the same block, in the order you sent them, or none land at all. AllenHark Relay submits bundles directly to the Jito Block Engine (sendBundle), choosing the lowest-latency regional block engine automatically and returning Jito's bundle id so you can track the outcome.

Use bundles when partial execution is unacceptable: multi-leg arbitrage, backrunning your own swap, atomic setup + trade + cleanup sequences, or any flow where transaction 2 is worthless unless transaction 1 landed first.

Submitting Bundles

All three transports accept bundles with the same payload shape:

TransportHow
HTTPSPOST /v1/sendBundle with {"txs": ["<base64>", ...], "id": "optional"}
QUICSame stream protocol as single txs — send {"txs": [...]} instead of {"tx": ...}
WebSocket/v1/stream/bundle frames — {"id": "b1", "txs": [...]}
1curl -X POST "https://relay.allenhark.com/v1/sendBundle?api-key=YOUR_KEY" \
2  -H "Content-Type: application/json" \
3  -d '{
4    "txs": ["BASE64_TX_1", "BASE64_TX_2"],
5    "id": "bundle-1"
6  }'

Response:

1{
2  "status": "accepted",
3  "bundle_id": "c9a8f2e1-...",
4  "signatures": ["5wH...", "3kP..."],
5  "jito_leader_in_slots": 2,
6  "id": "bundle-1"
7}

jito_leader_in_slots tells you how many slots away the next Jito-enabled leader is (0 = the current slot) — the relay tracks the Jito validator set automatically via Jito StakeNet. If no Jito leader is scheduled within the lookahead window, the response instead carries a warning field: the bundle was still submitted, but it is likely to expire, so plan to resubmit with a fresh blockhash.

bundle_id is the Jito bundle id — the same id Jito's own getBundleStatuses API knows the bundle by. The relay waits for block-engine acceptance (bounded ~1s) before responding, so an "accepted" response means the Block Engine has your bundle. If no channel accepts it, you get "status": "failed" instead.

Bundle Requirements — Two Tips

Every bundle needs two separate transfers (plain SystemProgram transfers, in any of the bundle's transactions):

  1. AllenHark relay tip — minimum 0.001 SOL to any registered tip wallet, same as single transactions. One transaction paying it covers the whole bundle. Dedicated relay customers skip this.
  2. Jito tip — minimum 1,000 lamports to one of Jito's tip accounts. Required for everyone, dedicated customers included — the Block Engine drops tipless bundles. During competitive periods, 10,000–100,000+ lamports is realistic; the Jito tip is an auction bid, not a flat fee.

Jito's 8 mainnet tip accounts (rotate randomly between them):

96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5 HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49 DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL 3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT

Additional rules:

  • All transactions fully signed, base64-encoded, ≤ ~1.6KB each
  • All transactions should use a fresh, shared recent blockhash
  • Both tips must be statically verifiable — no ALT-obscured transfers. Non-dedicated bundles may not contain ALT transactions at all; dedicated bundles may, but the Jito tip itself must use static account keys
  • Put the Jito tip in the last transaction (or as the last instruction) — if it's first, the rest of the bundle can be simulated against your paid tip

Landing Semantics

Bundles only land when the current slot leader runs the Jito-Solana client (~80%+ of stake). An accepted bundle that hits a stretch of non-Jito leaders will simply expire when its blockhash does. This is normal Jito behavior, not a relay failure:

  • Accepted ≠ landed. Track the outcome: confirm any of the returned signatures on-chain, or query Jito's getBundleStatuses with the bundle_id.
  • Expired? Re-sign with a fresh blockhash and resubmit. Same-signature resubmission is safe — bundle ids derive from the transaction signatures, and on-chain signature dedup guarantees at-most-once execution.
  • Failed/Invalid? One of the transactions errors when executed atomically (bundles are all-or-nothing — a revert anywhere drops the whole bundle) or a tip requirement wasn't met server-side.

[!TIP] Bundles are deliberately not fanned out through the TPU/UDP/RPC channels — splitting a bundle into individual transactions would silently destroy its atomicity. The Block Engine path is the only one that preserves the all-or-nothing guarantee.


Keep-Alive Best Practices

QUIC Keep-Alive

Recommended settings:

  • Keep-alive ping: every 3 seconds
  • Timeout: 10–15 seconds

Benefits:

  • No reconnection cost
  • Predictable 0.1ms dispatch
  • Session stability for high-frequency bots
1const client = new QuicClient("relay.allenhark.com:4433", {
2  keepAlive: true,
3  keepAliveInterval: 3000,
4  keepAliveTimeout: 15000,
5});

HTTPS Keep-Alive

Use the /keepalive endpoint to keep your HTTP connection warm and monitor server status:

1import axios from "axios";
2import https from "https";
3
4const agent = new https.Agent({ keepAlive: true });
5
6const api = axios.create({
7  baseURL: "https://relay.allenhark.com",
8  httpsAgent: agent,
9});
10
11// Poll every 5-10 seconds to maintain connection
12setInterval(async () => {
13  const { data } = await api.get("/keepalive");
14  console.log("Server time:", data.server_time, "Slot:", data.slot);
15}, 5000);

Response:

1{
2  "status": "alive",
3  "server_time": "2026-05-02T12:00:00.000Z",
4  "region": "fra",
5  "slot": 312345678
6}

This keeps the TCP connection alive, avoids repeated TLS handshakes, and gives you real-time server status.


Response Format

Success Response

1{
2  "status": "accepted",
3  "request_id": "550e8400-e29b-41d4-a716-446655440000",
4  "signature": "5wH...txSig"
5}

The relay returns the transaction signature for immediate confirmation tracking.

Error Responses

Error CodeDescription
tip_missingTransaction does not include a tip to AllenHark wallet
tip_insufficientTip amount below minimum threshold (1,000,000 lamports)
tip_hidden_in_altTransaction uses Address Lookup Table — cannot verify tip
jito_tip_missingBundle has no transfer to a Jito tip account (bundles only)
jito_tip_insufficientJito tip below the 1,000 lamport minimum (bundles only)
simulate_forbiddenThe simulate parameter must be false
invalid_api_keyAPI key not found or invalid
rate_limit_exceededRequest rate limit exceeded for this API key
invalid_transactionTransaction deserialization failed

Error Response Format

1{
2  "status": "rejected",
3  "error": "Transaction uses ALT — cannot verify tip"
4}

Performance Metrics

Broadcast Pipeline

Every transaction is delivered through 5 parallel channels for maximum inclusion probability:

ChannelProtocolDescription
Stake-Weighted QUICQUICPriority delivery across multiple global endpoints
QUIC TPUQUICDirect to next 48 validator leaders
UDP TPUUDPFire-and-forget to leaders
RPC Fan-outHTTPSParallel sendTransaction to all RPC nodes
ROC RelayersHTTPSJito Block Engine + custom relayers

Latency Breakdown

ComponentQUICHTTPS
Connection Setup0ms (persistent)15-20ms
Serialization0.1ms0.1ms
Network Transit0.3-0.5ms2-5ms
Relay Processing0.1-0.2ms0.1-0.2ms
Total0.1ms15-25ms

Throughput

  • QUIC: Up to 10,000 tx/s per connection
  • HTTPS: Up to 1,000 tx/s per connection

Co-Locate With Us for True 0.1ms Performance

For the absolute lowest latency, we strongly encourage clients to co-locate their bots or trading engines with us.

Colocation Benefits

  • Direct, private, ultra-fast link to the relay
  • True sub-millisecond transaction dispatch
  • Leader-aware execution
  • Guaranteed throughput
  • Access to private QUIC lanes
  • Priority engineering support

Colocation Locations

  • Frankfurt: TeraSwitch

Get in touch with us to secure a colocation slot.

Contact Us | View Co-Location Details


Monitoring

Ping Service

To verify connectivity to the relay, use the ping endpoint:

1# Primary (Frankfurt)
2curl https://relay.allenhark.com/ping
3
4# Regional
5curl https://fra.relay.allenhark.com/ping
6curl https://ams.relay.allenhark.com/ping
7curl https://ny.relay.allenhark.com/ping
8curl https://tyo.relay.allenhark.com/ping

Response:

pong

Best Practices

  1. Use QUIC for Production: Always use QUIC for production trading systems
  2. Implement Retry Logic: Handle network errors with exponential backoff
  3. Monitor Tip Amounts: Increase tips during network congestion
  4. Keep Connections Alive: Maintain persistent connections for best performance
  5. Use Nearest Region: Connect to the relay region closest to your infrastructure
  6. Co-locate if Possible: For true 0.1ms, co-locate your infrastructure
  7. Test on Devnet First: Always test integration on devnet before mainnet

Support

For technical support or colocation inquiries: