Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions benchmarks/solana-tx-landing-latency.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
slug: solana-tx-landing-latency
number: "027"
title: Solana transaction landing latency
seo_title: "Solana tx landing latency 2026: Jito vs Helius vs Nozomi vs Astralane time-to-land in slots"
seo_description: "Live active benchmark of Solana transaction landing services. Slot-level time-to-land (canonical on-chain measurement) plus wall-clock milliseconds. Identical signed mainnet probes submitted to each service from us-east every hour. Methodology pre-registered."
seo_title: "Solana tx landing latency 2026: Jito vs Helius vs Nozomi vs Astralane vs Mobula time-to-land in slots"
seo_description: "Live active benchmark of Solana transaction landing services. Slot-level time-to-land (canonical on-chain measurement) plus wall-clock milliseconds. Identical signed mainnet probes submitted to each service (Jito, Helius Sender, Astralane, Nozomi, Mobula) from us-east every hour. Methodology pre-registered."
subtitle: How fast does each landing service get a signed mainnet tx confirmed. Slot delta = number of Solana slots between submit and confirmed (1 slot ≈ 400 ms). Active probing, identical signed txs every hour from us-east.
category: Trading
status: live
Expand Down Expand Up @@ -206,3 +206,16 @@ providers:
success: sum(rate(solana_landing_probe_success_total{service="nozomi",region="us-east"}[7d])) / (sum(rate(solana_landing_probe_success_total{service="nozomi",region="us-east"}[7d])) + sum(rate(solana_landing_probe_dropped_total{service="nozomi",region="us-east",reason="timeout"}[7d])))
sample_size: sum(increase(solana_landing_probe_success_total{service="nozomi",region="us-east"}[7d]))
series: solana_landing_probe_latency_slots{service="nozomi",region="us-east"}

- slug: mobula
name: Mobula Swap-Send
tag: Multi-RPC fan-out aggregator (relays via Jito / Nozomi / zeroslot)
formula: "50th percentile over 7d of slot delta (land_slot − submit_slot) for hourly signed probes submitted via Mobula's `api.mobula.io/api/2/swap/send` (multi-RPC fan-out) from us-east. Tip wallet reuses Jito's because Mobula default-routes to Jito; attribution is via signatureSubscribe, independent of the tip wallet."
queries:
p50: quantile_over_time(0.5, solana_landing_probe_latency_slots{service="mobula",region="us-east"}[7d])
p90: quantile_over_time(0.9, solana_landing_probe_latency_slots{service="mobula",region="us-east"}[7d])
p99: quantile_over_time(0.99, solana_landing_probe_latency_slots{service="mobula",region="us-east"}[7d])
mean: avg_over_time(solana_landing_probe_latency_slots{service="mobula",region="us-east"}[7d])
success: sum(rate(solana_landing_probe_success_total{service="mobula",region="us-east"}[7d])) / (sum(rate(solana_landing_probe_success_total{service="mobula",region="us-east"}[7d])) + sum(rate(solana_landing_probe_dropped_total{service="mobula",region="us-east",reason="timeout"}[7d])))
sample_size: sum(increase(solana_landing_probe_success_total{service="mobula",region="us-east"}[7d]))
series: solana_landing_probe_latency_slots{service="mobula",region="us-east"}
110 changes: 110 additions & 0 deletions harnesses/solana-tx-landing/cmd/script/senders.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ func buildProbes(enabled map[Service]bool) ([]serviceProbe, error) {
buildNozomiProbe(tipWallets[ServiceNozomi]),
buildAstralaneProbe(tipWallets[ServiceAstralane]),
buildZeroSlotProbe(tipWallets[Service0slot]),
// Mobula has no own tip wallet (relays via Jito/Nozomi/zeroslot internally).
// Embed a Jito tip transfer so Mobula's default routing sees a valid tip;
// attribution uses signatureSubscribe (active probe path), not tip-wallet.
buildMobulaProbe(tipWallets[ServiceJito]),
}

out := make([]serviceProbe, 0, len(candidates))
Expand Down Expand Up @@ -439,3 +443,109 @@ func truncate(s string, n int) string {
}
return s[:n] + "…"
}

// -----------------------------------------------------------------------------
// Mobula swap-send — multi-RPC fan-out aggregator
// docs.mobula.io/rest-api-reference/endpoint/swap-send
//
// Wire shape (verified 2026-05-27):
// - POST https://api.mobula.io/api/2/swap/send
// - Authorization: Bearer <MOBULA_API_KEY>
// - Body: {"chainId": "solana:solana", "signedTransaction": "<base64>"}
// - Response on success: {"data": {"transactionHash": "<sig>", "requestId": "...",
// "success": true, "lander": "jito|nozomi|..."}}
// - 201 returned for accepted submission; downstream failure surfaces inside
// the JSON body via `error` / `data.success=false`.
//
// Mobula relays internally to Jito / Nozomi / zeroslot via multi-RPC fan-out,
// so there is no dedicated Mobula tip wallet. The probe embeds a Jito tip
// transfer (same wallet as the Jito control probe) so Mobula's default
// routing sees a valid tip on the underlying lander. Active-probe attribution
// works via signatureSubscribe on the tx signature, independent of the tip
// wallet used.
// -----------------------------------------------------------------------------

func buildMobulaProbe(tip solana.PublicKey) serviceProbe {
key := strings.TrimSpace(os.Getenv("MOBULA_API_KEY"))
if key == "" {
fmt.Println("[prober] mobula: MOBULA_API_KEY not set — service skipped")
return serviceProbe{}
}
url := envDefault("MOBULA_ENDPOINT", "https://api.mobula.io/api/2/swap/send")
tipLamports := uint64(envInt("MOBULA_TIP_LAMPORTS", 10_000))

return serviceProbe{
Service: ServiceMobula,
Mode: "default",
Endpoint: url,
TipWallet: tip,
TipLamports: tipLamports,
Sender: func(ctx context.Context, tx *solana.Transaction) (string, error) {
return postMobulaSwapSend(ctx, url, key, tx)
},
}
}

func postMobulaSwapSend(ctx context.Context, url, apiKey string, tx *solana.Transaction) (string, error) {
txB64, err := txToBase64(tx)
if err != nil {
return "", err
}
reqBody := map[string]any{
"chainId": "solana:solana",
"signedTransaction": txB64,
}
buf, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("marshal req: %w", err)
}
cctx, cancel := context.WithTimeout(ctx, senderHTTPTimeout)
defer cancel()
req, err := http.NewRequestWithContext(cctx, http.MethodPost, url, bytes.NewReader(buf))
if err != nil {
return "", fmt.Errorf("new req: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)

resp, err := senderHTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("http: %s", scrubSecrets(err.Error()))
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

if resp.StatusCode == 419 || resp.StatusCode == 429 {
return "", fmt.Errorf("http %d (rate limit): %s",
resp.StatusCode, scrubSecrets(truncate(string(respBody), 200)))
}
if resp.StatusCode >= 500 {
return "", fmt.Errorf("http %d: %s",
resp.StatusCode, scrubSecrets(truncate(string(respBody), 200)))
}

var parsed struct {
Data struct {
TransactionHash string `json:"transactionHash"`
Success bool `json:"success"`
} `json:"data"`
Error string `json:"error"`
}
if err := json.Unmarshal(respBody, &parsed); err != nil {
return "", fmt.Errorf("decode resp (status=%d body=%s): %w",
resp.StatusCode, scrubSecrets(truncate(string(respBody), 200)), err)
}
if parsed.Error != "" {
return "", fmt.Errorf("mobula: %s", scrubSecrets(parsed.Error))
}
if parsed.Data.TransactionHash != "" {
return parsed.Data.TransactionHash, nil
}
// Mobula sometimes returns 201 without echoing the sig. Fall back to
// the locally-computed signature (known once tx.Sign ran).
if len(tx.Signatures) == 0 {
return "", fmt.Errorf("no signature in response or on tx (status=%d body=%s)",
resp.StatusCode, scrubSecrets(truncate(string(respBody), 200)))
}
return tx.Signatures[0].String(), nil
}
4 changes: 4 additions & 0 deletions harnesses/solana-tx-landing/cmd/script/wallets.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const (
ServiceNextBlock Service = "nextblock"
ServiceAstralane Service = "astralane"
ServiceSolanaVibeStation Service = "solanavibestation"
// Mobula swap-send. No dedicated tip wallets — Mobula relays internally
// to Jito/Nozomi/zeroslot via multi-RPC fan-out. Not observable via
// tip-wallet attribution (bench № 016); active-probe-only (bench № 027).
ServiceMobula Service = "mobula"
)

// walletToService maps every known tip address to its owning service.
Expand Down
Loading