| |
| """ |
| jito_tip_observer.py |
| |
| READ-ONLY. Fetches Jito's public tip floor / percentile data so you can see |
| the real current tip market instead of inferring it from your own bot's |
| occasional 99th-percentile-floor numbers. |
| |
| Does NOT submit bundles, does NOT touch a wallet, does NOT place trades. |
| |
| Usage: |
| python3 jito_tip_observer.py |
| python3 jito_tip_observer.py --watch 30 # poll every 30s, Ctrl+C to stop |
| |
| Data source: Jito Labs' public bundle tip-floor API |
| (https://bundles.jito.wtf / mainnet.block-engine.jito.wtf), which publishes |
| rolling percentile tip stats across recent landed bundles. If Jito changes |
| or retires this endpoint, this script will fail loudly rather than silently |
| returning fake data -- check https://docs.jito.wtf for the current URL. |
| """ |
|
|
| import argparse |
| import json |
| import sys |
| import time |
| import urllib.request |
| import urllib.error |
|
|
| TIP_FLOOR_URL = "https://bundles.jito.wtf/api/v1/bundles/tip_floor" |
|
|
| LAMPORTS_PER_SOL = 1_000_000_000 |
|
|
|
|
| def fetch_tip_floor(): |
| req = urllib.request.Request(TIP_FLOOR_URL, headers={"User-Agent": "tip-observer/1.0"}) |
| with urllib.request.urlopen(req, timeout=10) as resp: |
| raw = resp.read() |
| return json.loads(raw) |
|
|
|
|
| def fmt_sol(lamports_val: float) -> str: |
| return f"{lamports_val / LAMPORTS_PER_SOL:.9f} SOL" |
|
|
|
|
| def print_snapshot(data): |
| |
| if isinstance(data, list): |
| if not data: |
| print("Empty response from Jito tip-floor API.") |
| return |
| row = data[0] |
| else: |
| row = data |
|
|
| print("-" * 70) |
| print(f"Jito tip floor snapshot (fetched {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())})") |
| print("-" * 70) |
|
|
| |
| |
| interesting = { |
| "landed_tips_25th_percentile": "25th pct", |
| "landed_tips_50th_percentile": "50th pct (median)", |
| "landed_tips_75th_percentile": "75th pct", |
| "landed_tips_95th_percentile": "95th pct", |
| "landed_tips_99th_percentile": "99th pct", |
| "ema_landed_tips_50th_percentile": "EMA 50th pct", |
| } |
|
|
| any_matched = False |
| for key, label in interesting.items(): |
| if key in row: |
| any_matched = True |
| val_sol = row[key] |
| lamports = round(val_sol * LAMPORTS_PER_SOL) |
| print(f" {label:22s}: {lamports:>10d} lamports ({val_sol:.9f} SOL)") |
|
|
| if not any_matched: |
| print(" (schema didn't match expected fields -- raw response below)") |
| print(json.dumps(row, indent=2)) |
| return |
|
|
| print() |
| p99 = row.get("landed_tips_99th_percentile") |
| p50 = row.get("landed_tips_50th_percentile") |
| if p99 and p50: |
| print(f" Note: to reliably land, you're historically bidding near the " |
| f"99th pct ({round(p99*LAMPORTS_PER_SOL)} lamports), " |
| f"~{p99/p50:.1f}x the median.") |
| print(" Compare this to your bot's actual profit-per-opportunity to see if") |
| print(" the tip market is even clearable at your trade size right now.") |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--watch", type=int, default=0, |
| help="Poll repeatedly every N seconds instead of once") |
| args = ap.parse_args() |
|
|
| def one_shot(): |
| try: |
| data = fetch_tip_floor() |
| except urllib.error.URLError as e: |
| print(f"Failed to reach Jito tip-floor API: {e}", file=sys.stderr) |
| print("Check network access / whether the endpoint has moved " |
| "(see https://docs.jito.wtf).", file=sys.stderr) |
| sys.exit(1) |
| except json.JSONDecodeError as e: |
| print(f"Got a response but couldn't parse JSON: {e}", file=sys.stderr) |
| sys.exit(1) |
| print_snapshot(data) |
|
|
| if args.watch > 0: |
| try: |
| while True: |
| one_shot() |
| print() |
| time.sleep(args.watch) |
| except KeyboardInterrupt: |
| print("\nStopped.") |
| else: |
| one_shot() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|