Lunar Lander

Hello Moon's high performance Solana transaction landing service

Overview

Landing Solana transactions quickly is critical to ensure that you get the best pricing and execution. Our Lunar Lander product is designed to land transactions as fast as possible

How it works

Lunar Lander uses advanced optimizations and performance enhancements to land Solana transactions quicker than a normal RPC endpoint. You construct your Solana transaction as normal, add a "tip" for Lunar Lander, and send it to our endpoint. We then submit it to the blockchain and land it as fast as possible.

You add a tip to your transactions by using a Solana transfer instruction. Send the amount that you wish to tip to one of our tip accounts, which are specified below.

Endpoints


RegionEndpoint
Amsterdam, Netherlandshttp://ams.lunar-lander.hellomoon.io/send
Ashburn, Virginia, United Stateshttp://ash.lunar-lander.hellomoon.io/send
Chicago, United Stateshttp://chi.lunar-lander.hellomoon.io/send
Dallas, United Stateshttp://dal.lunar-lander.hellomoon.io/send
Dubai, United Arab Emirateshttp://uae.lunar-lander.hellomoon.io/send
Frankfurt, Germanyhttp://fra.lunar-lander.hellomoon.io/send
London, United Kingdomhttp://lon.lunar-lander.hellomoon.io/send
Los Angeles, United Stateshttp://lax.lunar-lander.hellomoon.io/send
New York, United Stateshttp://nyc.lunar-lander.hellomoon.io/send
Seattle, United Stateshttp://sea.lunar-lander.hellomoon.io/send
Singaporehttp://sgp.lunar-lander.hellomoon.io/send
Sydney, Australiahttp://syd.lunar-lander.hellomoon.io/send
Tokyo, Japanhttp://tyo.lunar-lander.hellomoon.io/send
Vilnius, Lithuaniahttp://lit.lunar-lander.hellomoon.io/send

Geolocated to use the closest endpoint - http://lunar-lander.hellomoon.io/send

  • Note: This will be slightly slower than specific regions endpoints, so you should always use a region-specific one if you can.

Use this endpoint with sendTransaction like you would with a normal Solana RPC endpoint.

API Keys

Get your API key

Get your API key by opening a ticket on our Discord.

Using your API key

Specify your API key by passing it as a query parameter after the endpoint you are using. For example:

http://fra.lunar-lander.hellomoon.io/send?api-key=<your-api-key-here>

CORS-enabled endpoints

If you are sending transactions and need a CORS-enabled endpoint:

  • /send-cors — same as /send but with CORS headers
  • /send-bin-cors — same as /send-bin but with CORS headers

These endpoints return Access-Control-Allow-Origin: * and handle OPTIONS preflight requests. The existing /send and /send-bin endpoints are unchanged and do not include CORS headers.

Example:

http://fra.lunar-lander.hellomoon.io/send-cors?api-key=<your-api-key-here>

Binary transactions

You can also submit a raw serialized version of your transactions if you wish. This may be faster than JSON-RPC since there is less processing required.

Submit your transaction to the /send-bin endpoint in a POST request. The body of the request should be the serialized transaction

If you use a Content-Type header, it must application/octet-stream

The endpoint will return an HTTP 200 along with the transaction signature on submission or HTTP 400 and an error message if there was an issue.

Batch Send API

Submit multiple independent transactions in a single request using a compact binary wire format. Unlike bundles, batch transactions are processed individually — each transaction is sent independently and there is no atomic execution guarantee.

See the Batch Send API page for full details on wire format, streaming semantics, and response contract.

Send Bundle API

Send multiple transactions as an atomic bundle via the /sendBundle endpoint. Bundles are executed atomically, all transactions succeed or none are committed. This requires the sendbundle scope to be enabled on your API key. Contact us to activate it.

See the Send Bundle API page for full details on request format, tip requirements, and examples.

MEV Protection

Sandwich attacks occur when an attacker sees your pending transaction, places a trade before it to move the price, and then profits from the price movement your transaction causes. Lunar Lander's MEV protection feature, and some of the guidance below, can help prevent your transactions from being sandwiched.

How to enable: add one of the following to your request:

  • Query parameter: ?mev-protect=true (or ?mev-protect=1)
  • Header: X-Mev-Protect: true (or X-Mev-Protect: 1)

Example:

http://fra.lunar-lander.hellomoon.io/send?api-key=<your-api-key>&mev-protect=true

Or with a header:

POST /send?api-key=<your-api-key>
X-Mev-Protect: true

Supported endpoints: /send, /send-bin, /send-cors, /send-bin-cors, /sendBatch, /sendbatch.

Not supported: /sendBundle bundles are relayed through Jito/Harmonic which provide inherent MEV protection.

Front-Running Protection (jitodontfront)

The Jito block engine supports a front-running protection mechanism that prevents your transaction from being sandwiched inside a bundle. To use it, add any valid Solana public key beginning with jitodontfront as a read-only account to any instruction in your transaction. For example:

  • jitodontfront111111111111111111111111111111

The account does not need to exist on-chain. Marking it read-only avoids write lock contention, which helps landing speed. Address Lookup Tables (ALTs) are supported.

When the block engine encounters a transaction with a jitodontfront account, it rejects any bundle containing that transaction unless the transaction is at index 0 (first position). This prevents attackers from placing a trade before yours within a Jito bundle.

This protection applies to both single transactions (via sendTransaction) and bundles (via sendBundle). You can also use your own custom variation of the pubkey for per-application tracking.

Slippage Control

MEV protection at the infrastructure level reduces your exposure, but you should also set tight slippage tolerances on your swaps. Even with leader filtering and jitodontfront, your transaction still lands on-chain and is visible after execution. Setting appropriate slippage bounds ensures your transaction fails rather than executing at an unfavorable price if market conditions move between signing and landing.

Combining Protections

For maximum MEV protection, use all three together:

  1. mev-protect=true avoids sending your transaction to known sandwich validators via QUIC
  2. jitodontfront account prevents front-running within Jito bundles
  3. Tight slippage ensures your transaction reverts rather than executing at a bad price

Keep-Alive

GET /ping

Returns 200 "pong" with Content-Type: text/plain.

Why

Persistent connections to Lunar Lander pass through multiple hops (CloudFlare, nginx, upstream), each with its own idle timeout. If a connection sits idle too long, it gets closed. Reconnecting adds latency to the next transaction send.

The /ping endpoint lets you periodically touch the connection to keep it alive.

Recommended Interval

Every 30-45 seconds. Do not exceed 60 seconds between pings.

The binding constraint is nginx's 75-second keepalive timeout. Pinging every 30-45s provides a comfortable margin.

Important

The ping must be sent on the same TCP connection you want to keep alive. Opening a new connection for the ping defeats the purpose. Most HTTP client libraries (e.g., reqwest in Rust, axios in JS with a keep-alive agent) reuse connections automatically when pointed at the same origin.

connection = http_keep_alive_connect("https://<endpoint>")

# Background ping loop
every 30 seconds:
    connection.get("/ping")

# Transaction sends use the same connection
connection.post("/send", transaction_data)

Tip accounts

To use Lunar Lander, you must send your tip to one of the following accounts:

  • moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F
  • moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c
  • moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V
  • moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u
  • moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93
  • moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s
  • moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj
  • moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8
  • moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA
  • moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C

The minimum tip amount is 0.001 SOL. Sending higher tips is a way to ensure that your transaction is landed even faster in times of congestion, since we process transactions in order of received as well as by tip amount.

Example

# Construct a transfer instruction to send the tip to Lunar Lander
tip_instruction = transfer(
	TransferParams(
  from_pubkey=payer.pubkey(), # your wallet
  to_pubkey=tip_wallet, # our tip account
  lamports=tip_amount # the tip amount
  )
)

# Build the message with both instructions
message = Message.new_with_blockhash(
  [
    other_instruction, # The other instruction(s) in your transaction you want to submit
    tip_instruction # The tip instruction you made above
  ],
  payer.pubkey(), # your wallet
  recent_blockhash # the recent blockhash
)

# ... now sign the messsage and send it to our endpoint!

use anyhow::{anyhow, Context, Result};
use base64::Engine;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    hash::Hash,
    message::Message,
    native_token::LAMPORTS_PER_SOL,
    pubkey::Pubkey,
    signature::{Keypair, Signature, Signer},
    system_instruction,
    transaction::Transaction,
};
use std::str::FromStr;

/// Minimum tip amount in lamports
const MIN_TIP_LAMPORTS: u64 = 1000000;

/// System Program ID
const SYSTEM_PROGRAM_ID: &str = "11111111111111111111111111111111";

/// Configuration loaded from environment
struct Config {
    landing_endpoint: String,
    tip_wallet: String,
    private_key: Option<String>,
}

impl Config {
    fn from_env() -> Result<Self> {
        dotenv::dotenv().ok();

        let landing_endpoint = std::env::var("LANDING_ENDPOINT")
            .context("LANDING_ENDPOINT not set in environment")?;
        let tip_wallet = std::env::var("LANDING_TIP_WALLET")
            .context("LANDING_TIP_WALLET not set in environment")?;
        let private_key = std::env::var("PRIVATE_KEY").ok();

        Ok(Self {
            landing_endpoint,
            tip_wallet,
            private_key,
        })
    }
}

/// Load a Keypair from a base58-encoded private key string
fn load_keypair_from_base58(base58_string: &str) -> Result<Keypair> {
    let key_bytes = bs58::decode(base58_string)
        .into_vec()
        .context("Failed to decode base58 private key")?;
    Keypair::from_bytes(&key_bytes).context("Failed to create keypair from bytes")
}

/// Create a demo transaction that includes:
/// 1. A simple self-transfer (placeholder for your actual transaction logic)
/// 2. A tip transfer to the landing service
fn create_demo_transaction(
    payer: &Keypair,
    recent_blockhash: Hash,
    tip_wallet: &Pubkey,
    tip_amount: u64,
) -> Result<Transaction> {
    if tip_amount < MIN_TIP_LAMPORTS {
        return Err(anyhow!(
            "Tip amount must be at least {} lamports",
            MIN_TIP_LAMPORTS
        ));
    }

    // Instruction 1: Your actual transaction logic goes here
    // This is a placeholder self-transfer of 0 lamports
    let demo_instruction = system_instruction::transfer(&payer.pubkey(), &payer.pubkey(), 0);

    // Instruction 2: Tip transfer to the landing service
    let tip_instruction = system_instruction::transfer(&payer.pubkey(), tip_wallet, tip_amount);

    // Create the transaction with both instructions
    let message = Message::new(&[demo_instruction, tip_instruction], Some(&payer.pubkey()));
    let mut transaction = Transaction::new_unsigned(message);
    transaction.sign(&[payer], recent_blockhash);

    Ok(transaction)
}

/// Print human-readable details of a transaction
fn print_transaction_details(transaction: &Transaction) {
    let message = &transaction.message;

    println!("{}", "=".repeat(60));
    println!("TRANSACTION DETAILS");
    println!("{}", "=".repeat(60));

    // Signatures
    println!("\n📝 SIGNATURES:");
    for (i, sig) in transaction.signatures.iter().enumerate() {
        let sig_str = sig.to_string();
        if sig == &Signature::default() {
            println!("  [{}] (empty/unsigned)", i);
        } else {
            println!("  [{}] {}", i, sig_str);
        }
    }

    // Message header
    let header = &message.header;
    println!("\n📋 MESSAGE HEADER:");
    println!(
        "  Required signatures: {}",
        header.num_required_signatures
    );
    println!(
        "  Read-only signed accounts: {}",
        header.num_readonly_signed_accounts
    );
    println!(
        "  Read-only unsigned accounts: {}",
        header.num_readonly_unsigned_accounts
    );

    // Account keys
    println!(
        "\n🔑 ACCOUNT KEYS ({} accounts):",
        message.account_keys.len()
    );
    for (i, key) in message.account_keys.iter().enumerate() {
        let key_str = key.to_string();
        let mut labels = Vec::new();

        if i < header.num_required_signatures as usize {
            labels.push("signer");
            if i < (header.num_required_signatures - header.num_readonly_signed_accounts) as usize {
                labels.push("writable");
            }
        } else {
            let readonly_unsigned_start =
                message.account_keys.len() - header.num_readonly_unsigned_accounts as usize;
            if i < readonly_unsigned_start {
                labels.push("writable");
            }
        }

        if key_str == SYSTEM_PROGRAM_ID {
            labels.push("System Program");
        }

        let label_str = if labels.is_empty() {
            String::new()
        } else {
            format!(" ({})", labels.join(", "))
        };
        println!("  [{}] {}{}", i, key_str, label_str);
    }

    // Recent blockhash
    println!("\n🔗 RECENT BLOCKHASH:");
    println!("  {}", message.recent_blockhash);

    // Instructions
    println!(
        "\n📦 INSTRUCTIONS ({} total):",
        message.instructions.len()
    );
    for (i, ix) in message.instructions.iter().enumerate() {
        let program_key = &message.account_keys[ix.program_id_index as usize];
        print!("\n  Instruction [{}]:", i);
        print!("\n    Program: {}", program_key);
        if program_key.to_string() == SYSTEM_PROGRAM_ID {
            println!(" (System Program)");
        } else {
            println!();
        }

        println!("    Accounts:");
        for (j, acc_idx) in ix.accounts.iter().enumerate() {
            let acc_key = &message.account_keys[*acc_idx as usize];
            println!("      [{}] {}", j, acc_key);
        }

        // Decode System Program transfer instruction
        if program_key.to_string() == SYSTEM_PROGRAM_ID && ix.data.len() >= 12 {
            let instruction_type = u32::from_le_bytes(ix.data[0..4].try_into().unwrap_or([0; 4]));
            if instruction_type == 2 {
                // Transfer instruction
                let lamports = u64::from_le_bytes(ix.data[4..12].try_into().unwrap_or([0; 8]));
                println!(
                    "    Data (decoded): Transfer {} lamports ({:.9} SOL)",
                    lamports,
                    lamports as f64 / LAMPORTS_PER_SOL as f64
                );
            } else {
                println!("    Data (hex): {}", hex_encode(&ix.data));
            }
        } else {
            println!("    Data (hex): {}", hex_encode(&ix.data));
        }
    }

    // Raw serialized
    let serialized = bincode::serialize(transaction).unwrap_or_default();
    let hex_serialized = hex_encode(&serialized);
    println!("\n📄 RAW SERIALIZED (hex):");
    println!("  {}", hex_serialized);
    println!("\n  Length: {} bytes", serialized.len());
    println!("{}", "=".repeat(60));
}

/// Convert bytes to hex string
fn hex_encode(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{:02x}", b)).collect()
}

/// Send a signed transaction via the landing endpoint using JSON-RPC sendTransaction method
fn send_via_landing_endpoint(
    endpoint: &str,
    transaction: &Transaction,
) -> Result<serde_json::Value> {
    let serialized = bincode::serialize(transaction).context("Failed to serialize transaction")?;
    let base64_encoded = base64::engine::general_purpose::STANDARD.encode(&serialized);

    // Build JSON-RPC request for sendTransaction
    let rpc_request = serde_json::json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "sendTransaction",
        "params": [
            base64_encoded,
            {
                "encoding": "base64"
            }
        ]
    });

    let client = reqwest::blocking::Client::new();
    let response = client
        .post(endpoint)
        .json(&rpc_request)
        .timeout(std::time::Duration::from_secs(30))
        .send()
        .context("Failed to send request to landing endpoint")?;

    let json: serde_json::Value = response.json().context("Failed to parse response JSON")?;
    Ok(json)
}

fn main() -> Result<()> {
    println!("=== Solana Transaction Landing Demo (Rust) ===\n");

    // Load configuration
    let config = Config::from_env()?;

    println!("Landing Endpoint: {}", config.landing_endpoint);
    println!("Tip Wallet: {}", config.tip_wallet);
    println!("Minimum Tip: {} lamports\n", MIN_TIP_LAMPORTS);

    // Parse tip wallet pubkey
    let tip_wallet = Pubkey::from_str(&config.tip_wallet)
        .context("Invalid tip wallet pubkey")?;

    // Check if a private key was provided
    let (payer, will_submit) = if let Some(ref pk) = config.private_key {
        println!("Private key found in .env - will submit transaction\n");
        let keypair = load_keypair_from_base58(pk)?;
        println!("Payer Pubkey: {}\n", keypair.pubkey());
        (keypair, true)
    } else {
        println!("No private key found in .env - running in demo mode only");
        println!("To submit transactions, add PRIVATE_KEY=<base58_string> to your .env file\n");
        let keypair = Keypair::new();
        println!("Demo Payer Pubkey: {}", keypair.pubkey());
        println!("(Note: This is a randomly generated keypair for demo purposes)\n");
        (keypair, false)
    };

    // Connect to Solana mainnet to get a recent blockhash - this can also be your node's RPC endpoint
    let rpc_client = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());

    // Get recent blockhash
    let recent_blockhash = rpc_client
        .get_latest_blockhash()
        .context("Failed to get recent blockhash")?;
    println!("Recent Blockhash: {}\n", recent_blockhash);

    // Create the transaction with tip
    println!("Creating transaction with tip instruction...");
    let transaction = create_demo_transaction(
        &payer,
        recent_blockhash,
        &tip_wallet,
        MIN_TIP_LAMPORTS,
    )?;

    println!("Transaction created successfully!\n");

    // Show detailed transaction breakdown
    print_transaction_details(&transaction);
    println!();

    if will_submit {
        // Send via landing endpoint using JSON-RPC sendTransaction
        println!("Sending transaction via landing endpoint...");
        match send_via_landing_endpoint(&config.landing_endpoint, &transaction) {
            Ok(result) => {
                println!("Response: {:?}", result);
                // Extract signature from JSON-RPC response
                if let Some(sig) = result.get("result").and_then(|r| r.as_str()) {
                    println!("Signature: {}", sig);
                    println!("View on Solscan: https://solscan.io/tx/{}", sig);
                }
            }
            Err(e) => println!("Error sending transaction: {}", e),
        }
    } else {
        println!("Demo complete! (transaction not submitted)");
        println!("\nTo submit transactions, add your private key to .env:");
        println!("PRIVATE_KEY=<your_base58_encoded_private_key>");
    }

    Ok(())
}

Frequently Asked Questions

Do I pay anything if my transaction doesn't land?

No! Because you pay for Lunar Lander using a transfer instruction in your own transaction, if the transaction does not land successfully, you won't pay anything.

Are there any limits in place?

You can send as many transactions as you like through the Lunar Lander system. Rate limits are in place to prevent abuse and repeatedly failing transactions. If you need higher limits, please contact us.