Sending via QUIC
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
Connect to the region host on port 4433. QUIC runs over UDP: your client resolves the name once when it opens the connection, then reuses that connection for every transaction.
fra.fastrelay.sh:4433 # Frankfurt (Primary)
ams.fastrelay.sh:4433 # Amsterdam
ny.fastrelay.sh:4433 # New York
tyo.fastrelay.sh:4433 # TokyoTLS 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
// Quinn / rustls 0.23 — minimal QUIC client config
let crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
.with_no_client_auth();
let client_config = quinn::ClientConfig::new(Arc::new(
QuicClientConfig::try_from(crypto).unwrap()
));Stream Protocol
Each transaction uses a bidirectional QUIC stream:
-
Client sends API key header (optional — send
\nif no key):api-key: YOUR_API_KEY\n -
Client sends JSON payload:
json{"tx": "BASE64_TRANSACTION", "simulate": false} -
Client closes write side (signals end of request)
-
Server responds:
json{ "status": "accepted", "request_id": "550e8400-e29b-41d4-a716-446655440000", "signature": "5wH...txSig" }
Bundle frames use the same stream protocol — send txs (an array) instead of tx in step 2:
{"txs": ["BASE64_TX_1", "BASE64_TX_2"], "id": "b1"}The response carries the Jito bundle id instead of a single signature:
{"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)
use quinn::{Endpoint, crypto::rustls::QuicClientConfig};
use std::sync::Arc;
// Skip server cert verification (relay uses Let's Encrypt)
struct SkipVerify;
impl rustls::client::danger::ServerCertVerifier for SkipVerify {
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()) }
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()) }
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()) }
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> { rustls::crypto::ring::default_provider().signature_verification_algorithms.supported_schemes() }
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure QUIC client — no ALPN, no client cert, skip server verify
let crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipVerify))
.with_no_client_auth();
let client_config = quinn::ClientConfig::new(Arc::new(
QuicClientConfig::try_from(crypto)?
));
let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
endpoint.set_default_client_config(client_config);
// Resolve the region host, then connect (the connection persists — reuse it for all transactions)
let addr = tokio::net::lookup_host("fra.fastrelay.sh:4433").await?.next().unwrap();
let conn = endpoint
.connect(addr, "fra.fastrelay.sh")?
.await?;
// Open bidirectional stream per transaction
let (mut send, mut recv) = conn.open_bi().await?;
// Send header (API key optional — just send newline if none)
send.write_all(b"\n").await?;
// Send transaction
let payload = serde_json::json!({
"tx": "BASE64_TRANSACTION",
"simulate": false
});
send.write_all(&serde_json::to_vec(&payload)?).await?;
send.finish()?;
// Read response
let response = recv.read_to_end(4096).await?;
let result: serde_json::Value = serde_json::from_slice(&response)?;
println!("Response: {}", result);
Ok(())
}Python Example (aioquic)
import asyncio
import json
from aioquic.asyncio import connect
from aioquic.quic.configuration import QuicConfiguration
async def send_transaction(api_key: str, tx_base64: str):
configuration = QuicConfiguration(is_client=True)
configuration.verify_mode = True
async with connect(
"fra.fastrelay.sh", # Frankfurt
4433,
configuration=configuration,
) as client:
# Open stream
stream_id = client._quic.get_next_available_stream_id()
# Send API key
client._quic.send_stream_data(
stream_id,
f"api-key: {api_key}\n".encode()
)
# Send transaction
payload = json.dumps({
"tx": tx_base64,
"simulate": False
})
client._quic.send_stream_data(stream_id, payload.encode(), end_stream=True)
# Read response
response_data = await client._quic.receive_stream_data(stream_id)
response = json.loads(response_data.decode())
print("Response:", response)
asyncio.run(send_transaction("YOUR_API_KEY", "BASE64_TX"))Node.js Note
Node.js QUIC support is experimental. Rust is recommended for QUIC integration, or use the HTTPS endpoint for Node.js applications.