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
| Region | Endpoint |
|---|---|
| Amsterdam, Netherlands | http://ams.lunar-lander.hellomoon.io/send |
| Ashburn, Virginia, United States | http://ash.lunar-lander.hellomoon.io/send |
| Chicago, United States | http://chi.lunar-lander.hellomoon.io/send |
| Dallas, United States | http://dal.lunar-lander.hellomoon.io/send |
| Dubai, United Arab Emirates | http://uae.lunar-lander.hellomoon.io/send |
| Frankfurt, Germany | http://fra.lunar-lander.hellomoon.io/send |
| London, United Kingdom | http://lon.lunar-lander.hellomoon.io/send |
| Los Angeles, United States | http://lax.lunar-lander.hellomoon.io/send |
| New York, United States | http://nyc.lunar-lander.hellomoon.io/send |
| Seattle, United States | http://sea.lunar-lander.hellomoon.io/send |
| Singapore | http://sgp.lunar-lander.hellomoon.io/send |
| Sydney, Australia | http://syd.lunar-lander.hellomoon.io/send |
| Tokyo, Japan | http://tyo.lunar-lander.hellomoon.io/send |
| Vilnius, Lithuania | http://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/sendbut with CORS headers/send-bin-cors— same as/send-binbut 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(orX-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)
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:
mev-protect=trueavoids sending your transaction to known sandwich validators via QUICjitodontfrontaccount prevents front-running within Jito bundles- 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:
moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3Fmoon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4cmoon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8VmoonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1umoonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284smoonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yjmoonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JAmoonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C
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.