QUIC Submission

Use Lunar Lander QUIC when you want the lowest-latency path for sending Solana transactions through Lunar Lander.

QUIC is designed for advanced integrations that:

  • Want to avoid HTTP overhead
  • Build and sign transactions locally
  • Can manage long-lived connections

If you want simpler request and response semantics, our HTTP endpoints may be a better fit.

What it does

Lunar Lander QUIC is a fire-and-forget transaction submission path:

  • One transaction per QUIC unidirectional stream
  • No JSON-RPC wrapper
  • No batch framing
  • No per-stream response body

Each valid transaction is handed into Lunar Lander's normal send pipeline after authentication and ingress checks.

Endpoint

Use the same regional Lunar Lander hostnames you use for HTTP, but connect over QUIC on UDP port 16888.

Example:

  • fra.lunar-lander.hellomoon.io:16888

Use the region closest to your sender. You can see available regions here.

API key

You cannot connect without an API key. If you don't have one, please reach out to the team for access.

Tip requirement

Lunar Lander QUIC is tip-enforced. Please see here for our tip accounts and tip minimums.

MEV Protection

You can enable MEV protection on your QUIC connection by embedding a custom X.509 extension in your client certificate. When the server sees this extension, it applies MEV protection to all transactions sent over that connection — the same blacklist filtering available via ?mev-protect=true on the HTTP endpoints.

MEV protection is per-connection. To change it, reconnect with or without the extension.

Using the official Hello Moon client

use lunar_lander_quic_client::{ClientOptions, LunarLanderQuicClient};

let client = LunarLanderQuicClient::connect_with_options(
    "fra.lunar-lander.hellomoon.io:16888",
    std::env::var("LUNAR_LANDER_API_KEY")?,
    ClientOptions {
        mev_protect: true,
        ..ClientOptions::default()
    },
)
.await?;

Custom client implementation

If you are building your own QUIC client, add a non-critical X.509 extension to the self-signed client certificate before connecting:

FieldValue
OID2.999.1.1
Criticalfalse
ValueDER-encoded BOOLEAN TRUE: 0x01 0x01 0xFF

The extension is optional. Connections without it behave normally with MEV protection disabled.

Rust example

use rcgen::CustomExtension;

// Inside your cert generation, before calling params.self_signed():
let mev_ext = CustomExtension::from_oid_content(
    &[2, 999, 1, 1],
    vec![0x01, 0x01, 0xFF], // DER BOOLEAN TRUE
);
params.custom_extensions.push(mev_ext);

Go example

import (
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
)

// Inside your certificate template, before calling x509.CreateCertificate():
template.ExtraExtensions = []pkix.Extension{
    {
        Id:       asn1.ObjectIdentifier{2, 999, 1, 1},
        Critical: false,
        Value:    []byte{0x01, 0x01, 0xFF}, // DER BOOLEAN TRUE
    },
}

How to send a transaction

Submission model:

  1. Connect over QUIC.
  2. Authenticate with your client certificate.
  3. Open one unidirectional stream.
  4. Write one serialized transaction payload.
  5. Finish the stream.

Expected behavior:

  • Valid transactions are accepted into the send pipeline.
  • Invalid or non-qualifying transactions are dropped.
  • The connection can remain open across many submissions.

There is no per-stream success response. Lunar Lander optimizes this path for submission latency rather than request/response ergonomics.

Using the official Hello Moon client

The example below shows how to submit to QUIC using the official Hello Moon Rust client. It is available at https://crates.io/crates/lunar-lander-quic-client

use lunar_lander_quic_client::LunarLanderQuicClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("LUNAR_LANDER_API_KEY")?;
    let client = LunarLanderQuicClient::connect(
        "fra.lunar-lander.hellomoon.io:16888",
        api_key,
    )
    .await?;

    let tx_bytes: Vec<u8> = load_signed_transaction_bytes()?;
    client.send_transaction(&tx_bytes).await?;

    Ok(())
}
[dependencies]
lunar-lander-quic-client = "0.1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Example client application

Below is a Rust and Go example of how you can use QUIC to send transactions in your app if you don't want to use our client. This is only recommended for advanced users!

Both examples generate a self-signed client certificate in code and set CN=<YOUR_API_KEY> automatically.

If you prefer, you can still pre-generate certificate files and load them from disk, but that is not required.

use quinn::{ClientConfig, Endpoint};
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::{net::SocketAddr, sync::Arc};

const LUNAR_LANDER_TPU_PROTOCOL_ID: &[u8] = b"lunar-lander-tpu";
const SERVER_ADDR: &str = "fra.lunar-lander.hellomoon.io:16888";
const SERVER_NAME: &str = "fra.lunar-lander.hellomoon.io";

fn build_client_config(api_key: &str) -> anyhow::Result<ClientConfig> {
    let key_pair = KeyPair::generate()?;
    let mut params = CertificateParams::new(Vec::new())?;
    let mut distinguished_name = DistinguishedName::new();
    distinguished_name.push(DnType::CommonName, api_key);
    params.distinguished_name = distinguished_name;

    let certificate = params.self_signed(&key_pair)?;
    let private_key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));

    let mut rustls = rustls::ClientConfig::builder()
        .with_root_certificates(load_server_roots()?)
        .with_client_auth_cert(
            vec![CertificateDer::from(certificate.der().to_vec())],
            private_key,
        )?;

    rustls.alpn_protocols = vec![LUNAR_LANDER_TPU_PROTOCOL_ID.to_vec()];

    Ok(ClientConfig::new(Arc::new(
        quinn::crypto::rustls::QuicClientConfig::try_from(rustls)?
    )))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let api_key = std::env::var("LUNAR_LANDER_API_KEY")?;
    let server_addr: SocketAddr = SERVER_ADDR.parse()?;

    let client_config = build_client_config(&api_key)?;

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

    let connection = endpoint.connect(server_addr, SERVER_NAME)?.await?;

    let tx_bytes: Vec<u8> = load_signed_transaction_bytes()?;

    let mut send = connection.open_uni().await?;
    send.write_all(&tx_bytes).await?;
    send.finish()?;

    Ok(())
}
package main

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"math/big"
	"os"
	"time"

	quic "github.com/quic-go/quic-go"
)

const (
	ServerAddr = "fra.lunar-lander.hellomoon.io:16888"
	ServerName = "fra.lunar-lander.hellomoon.io"
	ALPN       = "lunar-lander-tpu"
)

func buildClientCertificate(apiKey string) (tls.Certificate, error) {
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return tls.Certificate{}, err
	}

	template := &x509.Certificate{
		SerialNumber: big.NewInt(time.Now().UnixNano()),
		Subject: pkix.Name{
			CommonName: apiKey,
		},
		NotBefore:             time.Now().Add(-time.Minute),
		NotAfter:              time.Now().Add(365 * 24 * time.Hour),
		KeyUsage:              x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
		BasicConstraintsValid: true,
	}

	certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
	if err != nil {
		return tls.Certificate{}, err
	}

	return tls.Certificate{
		Certificate: [][]byte{certDER},
		PrivateKey:  privateKey,
	}, nil
}

func main() {
	apiKey := os.Getenv("LUNAR_LANDER_API_KEY")
	if apiKey == "" {
		panic("LUNAR_LANDER_API_KEY is required")
	}

	clientCert, err := buildClientCertificate(apiKey)
	if err != nil {
		panic(err)
	}

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{clientCert},
		ServerName:   ServerName,
		NextProtos:   []string{ALPN},
		InsecureSkipVerify: true,
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	conn, err := quic.DialAddr(ctx, ServerAddr, tlsConfig, &quic.Config{
		// quic-go defaults to 1280-byte Initial packets; Lunar Lander expects 1200 here.
		InitialPacketSize: 1200,
	})
	if err != nil {
		panic(err)
	}
	defer conn.CloseWithError(0, "")

	txBytes, err := os.ReadFile("signed-tx.bin")
	if err != nil {
		panic(err)
	}

	stream, err := conn.OpenUniStreamSync(ctx)
	if err != nil {
		panic(err)
	}

	if _, err := stream.Write(txBytes); err != nil {
		panic(err)
	}

	if err := stream.Close(); err != nil {
		panic(err)
	}
}

ALPN

ALPN is the application protocol identifier negotiated during the QUIC/TLS handshake. Your client must send one of the supported values below.

Supported ALPN values:

  • lunar-lander-tpu

TLS server name

For native integrations, use the regional hostname as the TLS server name.

Example:

  • Endpoint: fra.lunar-lander.hellomoon.io:16888
  • Server name: fra.lunar-lander.hellomoon.io

Error behavior

Connection-level failures

These failures close the connection:

CodeReasonMeaning
1bad_certMissing or malformed certificate CN.
1invalid_authInvalid, revoked, or wrong-scope API key.
2connection_limitToo many concurrent connections for one API key.

Stream-level failures

These failures do not require the connection to close:

  • tip_required
  • invalid_payload
  • Oversized payload
  • Transaction-level rate-limit drop

These are treated as dropped submissions on the stream path. The connection may remain open and be reused.

Limits

SettingValue
Max transaction size1232 bytes
Max concurrent unidirectional streams per connection64
Idle timeout30s
Max concurrent connections per API key10
Max connection attempts per source IP10/min

Best practices

  • Reuse connections; do not reconnect for every transaction
  • Send one transaction per unidirectional stream
  • Use the closest region to your sender

FAQ

Does QUIC return a transaction signature?

No. Lunar Lander QUIC is fire-and-forget at the stream level.

Can I enable MEV protection over QUIC?

Yes. Embed a custom X.509 extension (OID 2.999.1.1) in your client certificate, or set mev_protect: true in ClientOptions if you are using the official Rust client. See MEV Protection above.

Can I send tipless transactions over QUIC?

No. Lunar Lander QUIC is tip-enforced.

Can I batch multiple transactions in one stream?

No. Send one transaction per unidirectional stream.

Are you compatible with any other QUIC clients?

Yes. Lunar Lander is drop-in compatible with several other QUIC clients.

Is there an official Hello Moon QUIC client library?

Yes, we have an official Rust client. You can get it at https://crates.io/crates/lunar-lander-quic-client