Skip to content

Comments

feat(sniper): add production-grade pump.fun sniper with Jito bundles#153

Closed
nightedgeio wants to merge 1 commit intochainstacklabs:mainfrom
nightedgeio:feat-2026-live
Closed

feat(sniper): add production-grade pump.fun sniper with Jito bundles#153
nightedgeio wants to merge 1 commit intochainstacklabs:mainfrom
nightedgeio:feat-2026-live

Conversation

@nightedgeio
Copy link

@nightedgeio nightedgeio commented Feb 2, 2026

Summary

  • Yellowstone Geyser gRPC listener for fast token detection
  • Jito bundle submission for MEV protection and sandwich resistance
  • Telegram notifications on buy/sell with PnL tracking
  • Environment variable configuration only (no YAML configs)
  • Single-token mode with fast exit after one complete trade

Structure

  • sniper/main.py - Entry point and bot orchestration
  • sniper/geyser.py - Geyser gRPC listener
  • sniper/trader.py - Trading logic with Jito support
  • sniper/.env.example - Configuration template

Test plan

  • Copy .env.example to .env and configure
  • Run python sniper/main.py
  • Verify Geyser connection
  • Test buy on new token detection
  • Verify Telegram notifications

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced Sniper Bot for automated Pump.fun token trading on Solana
    • Added real-time token detection and monitoring via Geyser streaming
    • Implemented multiple exit strategies: time-based hold, take-profit/stop-loss, and immediate sell
    • Added MEV protection through Jito bundle integration
    • Enabled Telegram notifications for trading events
    • Provided environment configuration system for bot setup and parameters

Production-grade standalone sniper implementation:

- Yellowstone Geyser gRPC listener for fast token detection
- Jito bundle submission for MEV protection and sandwich resistance
- Telegram notifications on buy/sell with PnL tracking
- Environment variable configuration only (no YAML)
- Single-token mode with fast exit after one trade
- Multiple exit strategies: time_based, tp_sl, immediate
- IDL-based instruction building for reliability
- Automatic tip account rotation for Jito
- Graceful shutdown handling

Structure:
- sniper/main.py - Entry point and bot orchestration
- sniper/geyser.py - Geyser gRPC listener
- sniper/trader.py - Trading logic with Jito support
- sniper/.env.example - Configuration template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

Introduces a Pump.fun token sniper bot for Solana across four new modules: environment configuration template, Yellowstone Geyser gRPC listener for token detection, MEV-protected trader with Jito bundle support, and orchestrating main entry point with configurable exit strategies and Telegram notifications.

Changes

Cohort / File(s) Summary
Configuration
sniper/.env.example
Environment variables documentation covering wallet setup, RPC/Geyser endpoints, Jito tipping, trading parameters (slippage, amounts), exit strategies, token filters, and notifications.
Token Detection
sniper/geyser.py
Yellowstone Geyser gRPC client that listens for Pump.fun token creation events, parses IDL-based instructions to extract token metadata, supports filtering by name/symbol/creator, and includes connection management with reconnection logic.
Trading Engine
sniper/trader.py
Pump.fun trader with MEV protection via Jito bundles, implements buy/sell flows with slippage protection, manages blockhash refresh cycles, includes instruction builders for token accounts and compute budgets, and provides Telegram notifications for trades.
Bot Orchestration
sniper/main.py
SniperBot orchestrator that wires together Geyser listening, trading execution, and Telegram notifications; implements three exit strategies (time-based, take-profit/stop-loss, immediate sell), handles graceful shutdown, and manages position lifecycle.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Bot as SniperBot
    participant Geyser as GeyserListener
    participant Blockchain as Solana RPC
    participant Jito as Jito Bundle
    participant Telegram as TelegramNotifier

    User->>Bot: start()
    Bot->>Geyser: listen(callback=_on_new_token)
    Geyser->>Geyser: connect gRPC
    Geyser->>Blockchain: subscribe to Pump.fun events
    
    Blockchain-->>Geyser: new token created
    Geyser->>Geyser: parse token metadata
    Geyser-->>Bot: callback(TokenInfo)
    
    Bot->>Bot: _on_new_token()
    Bot->>Blockchain: query pool state
    Bot->>Blockchain: buy transaction (via Jito)
    Jito->>Blockchain: submit bundle
    Blockchain-->>Jito: confirmed
    Jito-->>Bot: TradeResult
    
    Bot->>Telegram: notify_buy()
    
    Bot->>Bot: _execute_exit_strategy()
    
    alt Time-Based Exit
        Bot->>Bot: wait(hold_time)
        Bot->>Blockchain: sell transaction
        Blockchain-->>Bot: TradeResult
    else TP/SL Exit
        Bot->>Bot: monitor price
        loop Until TP or SL
            Bot->>Blockchain: query pool price
            Blockchain-->>Bot: current price
        end
        Bot->>Blockchain: sell transaction
        Blockchain-->>Bot: TradeResult
    else Immediate Exit
        Bot->>Blockchain: sell transaction
        Blockchain-->>Bot: TradeResult
    end
    
    Bot->>Telegram: notify_sell(pnl)
    Bot->>Bot: stop()
    Bot->>Geyser: stop()
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A sniper bot hops through the blockchain night,
Listening for tokens with Geyser's light,
Bundling with Jito to dodge the MEV,
Buying and selling with strategic view—
Exit strategies picked with care,
Hop, hop, profit everywhere! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely describes the main change: introducing a production-grade Pump.fun sniper bot with Jito bundle support for MEV protection.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nightedgeio
Copy link
Author

Hi! I generated a production upgrade to the sniper bot using Claude Code: gRPC Yellowstone Geyser listener, Jito bundles for MEV protection, Telegram alerts, env-only config, single-trade mode.
Please review and merge if it fits.
Thanks!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@sniper/geyser.py`:
- Around line 258-265: The listen method currently accepts max_token_age but
never uses it; update listen (and any helper that processes gRPC updates) to
filter out TokenInfo objects older than max_token_age by comparing the current
time to the transaction/slot timestamp available in the incoming update (use the
slot timestamp field on the gRPC update or whatever timestamp is populated on
the TokenInfo), and only invoke callback for tokens within that age window; if
the gRPC update does not expose a usable timestamp, remove the max_token_age
parameter from listen and related signatures (and update docs) instead of
leaving it unused.

In `@sniper/main.py`:
- Around line 225-231: Before calling self.trader.buy, call and await a new
get_sol_balance() method on PumpFunTrader to retrieve the wallet SOL balance,
compare it against self.buy_amount_sol (include a small fee buffer), and if the
balance is insufficient log/print a clear message and return without attempting
the buy; implement get_sol_balance() on PumpFunTrader to return the current SOL
balance so the check happens prior to using use_jito and invoking trader.buy.
- Around line 216-236: The async handler _on_new_token has f-strings with no
placeholders (e.g., print(f"\n[BOT] NEW TOKEN DETECTED!")) and is missing a
return type; change the signature to async def _on_new_token(self, token:
TokenInfo) -> None: and remove the unnecessary f prefix from literal-only prints
(or add real placeholders if intended) for all print statements in this function
(including those referencing TOKEN_DECIMALS and LAMPORTS_PER_SOL), and apply the
same fix to the other file locations that use f-strings without placeholders
(the other handler/print sites with the same pattern).

In `@sniper/trader.py`:
- Around line 151-154: The emoji assignment in sniper.trader (variable emoji
near sol_received/tokens calculations) uses an if-else that sets the same empty
string for both branches; either replace it with meaningful symbols (e.g., set
emoji = "🔺" when pnl_percent >= 0 and "🔻" when negative) or remove the emoji
variable and any references to it entirely if not needed; update usages
accordingly (look for symbol emoji and the surrounding logic that formats
pnl_percent, sol_received, tokens, TOKEN_DECIMALS, LAMPORTS_PER_SOL, and result)
so the code no longer contains a redundant conditional.
- Around line 592-598: The TradeResult is being populated with min_token_amount
(slippage-protected minimum) instead of the actual tokens received, which can
make PnL inaccurate; after sending and confirming the swap (you have the
signature variable), query the chain for the confirmed transaction or the token
account balance (e.g., via getTransaction/getConfirmedTransaction or
getTokenAccountBalance) to compute the actual received token amount and assign
that value to tokens_amount before constructing the TradeResult; update the code
around the TradeResult creation (where signature, min_token_amount,
sol_lamports, price_per_token are used) to replace min_token_amount with the
observed actual token amount, optionally falling back to min_token_amount if the
on-chain query fails.
- Around line 470-480: The _confirm_transaction method currently accepts a
timeout parameter but never uses it; update the implementation to enforce the
timeout by wrapping the await self._client.confirm_transaction(...) call with
asyncio.wait_for(..., timeout=timeout) (import asyncio if not present), and keep
the existing try/except behavior (return True on success, return False on
asyncio.TimeoutError or any Exception), or alternatively remove the timeout
parameter and its docstring if you prefer not to support timeouts; reference the
_confirm_transaction method and the self._client.confirm_transaction call when
making the change.
🧹 Nitpick comments (13)
sniper/.env.example (2)

116-117: Consider SKIP_PREFLIGHT=false as the safer default for production.

The .env.example sets SKIP_PREFLIGHT=true as default. While skipping preflight is faster, it bypasses validation that can catch issues (insufficient balance, invalid accounts) before submission. For a production template, false is safer—users can opt into true after testing.

Suggested change
 # Skip preflight checks for faster submission
-SKIP_PREFLIGHT=true
+SKIP_PREFLIGHT=false

42-46: Consider adding a MIN_SOL_BALANCE parameter.

Per coding guidelines, the bot should "implement wallet balance checks before trading." Adding a minimum SOL balance check prevents trading when the wallet is underfunded, avoiding failed transactions.

Suggested addition after line 46
 # Amount of SOL to spend on each buy (in SOL, not lamports)
 BUY_AMOUNT_SOL=0.05
+
+# Minimum SOL balance required to trade (safety check)
+MIN_SOL_BALANCE=0.1

Based on learnings: "Start with conservative YAML settings (low buy_amount, high min_sol_balance, strict filters)."

sniper/geyser.py (4)

1-10: Use centralized logger instead of print() statements.

The module uses print() throughout (lines 287-288, 312-314, 325, 329, etc.) but coding guidelines require using the centralized logger via from src.utils.logger import get_logger. This enables log levels, rotation, and structured logging for production.

Example refactor
+from src.utils.logger import get_logger
+
+logger = get_logger(__name__)
+
 # Later in code:
-print(f"[GEYSER] Connected to {self.endpoint}")
+logger.info("Connected to %s", self.endpoint)

As per coding guidelines: "Use a module-level logger via get_logger(name)."


370-373: Blind exception catch loses debugging context.

The except Exception at line 372 silently returns None, making it difficult to diagnose parsing failures. Per coding guidelines, exceptions should be logged with context.

Suggested fix
             if token_info:
                 return token_info
 
-            return None
-
-        except Exception:
             return None
+
+        except Exception:
+            # Log at debug level to avoid noise from non-create transactions
+            logger.debug("Error processing update", exc_info=True)
+            return None

Same pattern applies to lines 101-102 and 328-330.


55-74: Add error handling for IDL file loading.

The IDL file is opened without a try/except block. If the file is missing or malformed, the error will be unclear. Per coding guidelines, file I/O should be wrapped with proper error handling.

Suggested fix
     def __init__(self, idl_path: Path | None = None):
         """Initialize parser with IDL data."""
         if idl_path is None:
             idl_path = Path(__file__).parent.parent / "idl" / "pump_fun_idl.json"
 
-        with open(idl_path) as f:
-            self.idl = json.load(f)
+        try:
+            with open(idl_path) as f:
+                self.idl = json.load(f)
+        except FileNotFoundError:
+            raise ValueError(f"IDL file not found: {idl_path}") from None
+        except json.JSONDecodeError as e:
+            raise ValueError(f"Invalid IDL JSON in {idl_path}: {e}") from e

90-95: Line exceeds 88 character limit.

Line 92 is 92 characters, violating the configured Ruff line length limit of 88.

Suggested fix
         if discriminator == self._create_discriminator:
             is_v2 = False
-        elif self._create_v2_discriminator and discriminator == self._create_v2_discriminator:
+        elif (
+            self._create_v2_discriminator
+            and discriminator == self._create_v2_discriminator
+        ):
             is_v2 = True
sniper/main.py (2)

16-21: Use centralized logger instead of print() statements.

The module uses print() throughout but should use the centralized logger for production. This enables proper log levels, rotation, and structured logging.

Same pattern as noted in geyser.py - import and use get_logger(__name__). As per coding guidelines: "Use get_logger(name) with logger from utils.logger."


405-415: Log exceptions with traceback even in non-debug mode.

The main() function only prints the traceback when DEBUG=true. In production, stack traces are valuable for debugging failures. Consider always logging the exception details.

Suggested fix
     except Exception as e:
-        print(f"[ERROR] {e}")
-        if os.getenv("DEBUG", "false").lower() == "true":
-            import traceback
-            traceback.print_exc()
+        import traceback
+        print(f"[ERROR] {e}")
+        traceback.print_exc()
     finally:
         await bot.stop()

Or better, use a logger with logger.exception("Bot error") which automatically includes the traceback.

sniper/trader.py (5)

460-468: Add retry mechanism with exponential backoff for transaction submission.

Per coding guidelines, transaction submission should have retry mechanisms. Currently, _send_transaction_direct makes a single attempt and returns None on failure.

Suggested implementation
     async def _send_transaction_direct(self, transaction: Transaction) -> str | None:
         """Send transaction directly to RPC."""
-        try:
-            opts = TxOpts(skip_preflight=True, preflight_commitment=Processed)
-            resp = await self._client.send_transaction(transaction, opts)
-            return str(resp.value)
-        except Exception as e:
-            print(f"[TRADER] Direct send error: {e}")
-            return None
+        max_retries = 3
+        for attempt in range(max_retries):
+            try:
+                opts = TxOpts(skip_preflight=True, preflight_commitment=Processed)
+                resp = await self._client.send_transaction(transaction, opts)
+                return str(resp.value)
+            except Exception as e:
+                print(f"[TRADER] Direct send error (attempt {attempt + 1}/{max_retries}): {e}")
+                if attempt < max_retries - 1:
+                    await asyncio.sleep(0.5 * (2 ** attempt))  # Exponential backoff
+        return None

As per coding guidelines: "Implement retry mechanisms with exponential backoff for transaction submission/failures."


1-11: Use centralized logger instead of print() statements.

Like the other modules, this file uses print() throughout but should use the centralized logger for proper log management in production.

As per coding guidelines: "Use get_logger(name) with logger from utils.logger."


201-208: Add error handling for IDL file loading.

Same issue as in geyser.py - the IDL file is opened without error handling. If the file is missing, the error will be unclear.

Suggested fix
         # Load IDL for instruction building
         idl_path = Path(__file__).parent.parent / "idl" / "pump_fun_idl.json"
-        with open(idl_path) as f:
-            self.idl = json.load(f)
+        try:
+            with open(idl_path) as f:
+                self.idl = json.load(f)
+        except (FileNotFoundError, json.JSONDecodeError) as e:
+            raise ValueError(f"Failed to load IDL from {idl_path}: {e}") from e

119-125: Reuse aiohttp.ClientSession instead of creating one per request.

Creating a new ClientSession per send() call is inefficient and doesn't leverage connection pooling. Per coding guidelines, implement connection pooling for HTTP clients.

Suggested refactor
 class TelegramNotifier:
     """Send Telegram notifications."""

     def __init__(self, bot_token: str, chat_id: str):
         self.bot_token = bot_token
         self.chat_id = chat_id
         self.enabled = bool(bot_token and chat_id)
+        self._session: aiohttp.ClientSession | None = None
+
+    async def _get_session(self) -> aiohttp.ClientSession:
+        if self._session is None or self._session.closed:
+            self._session = aiohttp.ClientSession()
+        return self._session
+
+    async def close(self) -> None:
+        if self._session and not self._session.closed:
+            await self._session.close()

     async def send(self, message: str):
         """Send a message to Telegram."""
         if not self.enabled:
             return

         url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
         # ...
         try:
-            async with aiohttp.ClientSession() as session:
-                async with session.post(url, json=payload, timeout=10) as resp:
+            session = await self._get_session()
+            async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=10)) as resp:

As per coding guidelines: "Implement connection pooling for HTTP clients (e.g., aiohttp)."


521-523: Lines exceed 88 character limit.

Lines 521-522 exceed the configured 88-character limit.

Suggested fix
-            print(f"[TRADER] Expected tokens: {token_amount_raw / (10 ** TOKEN_DECIMALS):,.2f}")
-            print(f"[TRADER] Min tokens (with slippage): {min_token_amount / (10 ** TOKEN_DECIMALS):,.2f}")
+            expected_tokens = token_amount_raw / (10 ** TOKEN_DECIMALS)
+            min_tokens = min_token_amount / (10 ** TOKEN_DECIMALS)
+            print(f"[TRADER] Expected tokens: {expected_tokens:,.2f}")
+            print(f"[TRADER] Min tokens (with slippage): {min_tokens:,.2f}")

Comment on lines +258 to +265
async def listen(
self,
callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
max_token_age: float = 5.0,
stop_after_first: bool = True,
) -> TokenInfo | None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unused max_token_age parameter - advertised feature not implemented.

The max_token_age parameter is accepted but never used in the method body. The .env.example documents MAX_TOKEN_AGE_SECONDS=5 and implies tokens older than this should be skipped, but the filtering logic is missing.

Proposed implementation
     async def listen(
         self,
         callback: Callable[[TokenInfo], Awaitable[None]],
         match_string: str | None = None,
         creator_address: str | None = None,
         max_token_age: float = 5.0,
         stop_after_first: bool = True,
     ) -> TokenInfo | None:

The implementation would need access to the transaction's slot timestamp to compare against current time. If slot timestamps aren't available in the gRPC update, consider removing the parameter and documenting this limitation.

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 263-263: Unused method argument: max_token_age

(ARG002)

🤖 Prompt for AI Agents
In `@sniper/geyser.py` around lines 258 - 265, The listen method currently accepts
max_token_age but never uses it; update listen (and any helper that processes
gRPC updates) to filter out TokenInfo objects older than max_token_age by
comparing the current time to the transaction/slot timestamp available in the
incoming update (use the slot timestamp field on the gRPC update or whatever
timestamp is populated on the TokenInfo), and only invoke callback for tokens
within that age window; if the gRPC update does not expose a usable timestamp,
remove the max_token_age parameter from listen and related signatures (and
update docs) instead of leaving it unused.

Comment on lines +216 to +236
async def _on_new_token(self, token: TokenInfo):
"""Handle new token detection."""
print(f"\n[BOT] NEW TOKEN DETECTED!")
print(f"[BOT] Name: {token.name}")
print(f"[BOT] Symbol: {token.symbol}")
print(f"[BOT] Mint: {token.mint}")
print(f"[BOT] Creator: {token.creator}")
print(f"[BOT] Token2022: {token.is_token_2022}")

# Execute buy
use_jito = bool(self.jito_url)
result = await self.trader.buy(token, self.buy_amount_sol, use_jito=use_jito)

if not result.success:
print(f"[BOT] Buy failed: {result.error}")
return

print(f"[BOT] Buy successful!")
print(f"[BOT] Signature: {result.signature}")
print(f"[BOT] Tokens: {(result.tokens_amount or 0) / (10 ** TOKEN_DECIMALS):,.2f}")
print(f"[BOT] SOL spent: {(result.sol_amount or 0) / LAMPORTS_PER_SOL:.6f}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix f-strings without placeholders and add type hint.

Lines 218, 233 have f-strings with no placeholders. Also, the method lacks a return type annotation.

Proposed fix
-    async def _on_new_token(self, token: TokenInfo):
+    async def _on_new_token(self, token: TokenInfo) -> None:
         """Handle new token detection."""
-        print(f"\n[BOT] NEW TOKEN DETECTED!")
+        print("\n[BOT] NEW TOKEN DETECTED!")
         print(f"[BOT] Name: {token.name}")
         print(f"[BOT] Symbol: {token.symbol}")
         print(f"[BOT] Mint: {token.mint}")
         print(f"[BOT] Creator: {token.creator}")
         print(f"[BOT] Token2022: {token.is_token_2022}")

         # Execute buy
         use_jito = bool(self.jito_url)
         result = await self.trader.buy(token, self.buy_amount_sol, use_jito=use_jito)

         if not result.success:
             print(f"[BOT] Buy failed: {result.error}")
             return

-        print(f"[BOT] Buy successful!")
+        print("[BOT] Buy successful!")

Same issue on lines 327, 332, 358, 385.

🧰 Tools
🪛 Ruff (0.14.14)

[error] 218-218: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 233-233: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In `@sniper/main.py` around lines 216 - 236, The async handler _on_new_token has
f-strings with no placeholders (e.g., print(f"\n[BOT] NEW TOKEN DETECTED!")) and
is missing a return type; change the signature to async def _on_new_token(self,
token: TokenInfo) -> None: and remove the unnecessary f prefix from literal-only
prints (or add real placeholders if intended) for all print statements in this
function (including those referencing TOKEN_DECIMALS and LAMPORTS_PER_SOL), and
apply the same fix to the other file locations that use f-strings without
placeholders (the other handler/print sites with the same pattern).

Comment on lines +225 to +231
# Execute buy
use_jito = bool(self.jito_url)
result = await self.trader.buy(token, self.buy_amount_sol, use_jito=use_jito)

if not result.success:
print(f"[BOT] Buy failed: {result.error}")
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add wallet balance check before buying.

Per coding guidelines, trading logic should "implement wallet balance checks before trading." A buy attempt with insufficient SOL will fail, wasting time and potentially missing opportunities.

Suggested implementation
         # Execute buy
         use_jito = bool(self.jito_url)
+
+        # Check wallet balance before buying
+        required_sol = self.buy_amount_sol + (self.jito_tip_lamports / LAMPORTS_PER_SOL) + 0.01  # buffer for fees
+        balance = await self.trader.get_sol_balance()
+        if balance < required_sol * LAMPORTS_PER_SOL:
+            print(f"[BOT] Insufficient balance: {balance / LAMPORTS_PER_SOL:.4f} SOL < {required_sol:.4f} SOL required")
+            return
+
         result = await self.trader.buy(token, self.buy_amount_sol, use_jito=use_jito)

This requires adding a get_sol_balance() method to PumpFunTrader.

As per coding guidelines: "Implement wallet balance checks before trading."

🤖 Prompt for AI Agents
In `@sniper/main.py` around lines 225 - 231, Before calling self.trader.buy, call
and await a new get_sol_balance() method on PumpFunTrader to retrieve the wallet
SOL balance, compare it against self.buy_amount_sol (include a small fee
buffer), and if the balance is insufficient log/print a clear message and return
without attempting the buy; implement get_sol_balance() on PumpFunTrader to
return the current SOL balance so the check happens prior to using use_jito and
invoking trader.buy.

Comment on lines +151 to +154
sol_received = (result.sol_amount or 0) / LAMPORTS_PER_SOL
tokens = (result.tokens_amount or 0) / (10 ** TOKEN_DECIMALS)
emoji = "" if pnl_percent >= 0 else ""

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Useless if-else condition - both branches produce empty string.

Line 153 has identical outcomes in both branches. This appears to be missing emoji characters (possibly due to encoding or copy-paste issues).

Suggested fix
         sol_received = (result.sol_amount or 0) / LAMPORTS_PER_SOL
         tokens = (result.tokens_amount or 0) / (10 ** TOKEN_DECIMALS)
-        emoji = "" if pnl_percent >= 0 else ""
+        emoji = "🟢" if pnl_percent >= 0 else "🔴"

         msg = (
             f"<b>SELL</b> {token.symbol} {emoji}\n"

Or remove the emoji variable entirely if not needed.

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 153-153: Useless if-else condition

(RUF034)

🤖 Prompt for AI Agents
In `@sniper/trader.py` around lines 151 - 154, The emoji assignment in
sniper.trader (variable emoji near sol_received/tokens calculations) uses an
if-else that sets the same empty string for both branches; either replace it
with meaningful symbols (e.g., set emoji = "🔺" when pnl_percent >= 0 and "🔻"
when negative) or remove the emoji variable and any references to it entirely if
not needed; update usages accordingly (look for symbol emoji and the surrounding
logic that formats pnl_percent, sol_received, tokens, TOKEN_DECIMALS,
LAMPORTS_PER_SOL, and result) so the code no longer contains a redundant
conditional.

Comment on lines +470 to +480
async def _confirm_transaction(self, signature: str, timeout: float = 30) -> bool:
"""Wait for transaction confirmation."""
try:
await self._client.confirm_transaction(
signature,
commitment="confirmed",
sleep_seconds=0.5,
)
return True
except Exception:
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused timeout parameter in _confirm_transaction.

The timeout parameter is accepted but never used. The method should either use this parameter or remove it.

Suggested fix using the parameter
     async def _confirm_transaction(self, signature: str, timeout: float = 30) -> bool:
         """Wait for transaction confirmation."""
         try:
             await self._client.confirm_transaction(
                 signature,
                 commitment="confirmed",
-                sleep_seconds=0.5,
+                sleep_seconds=min(0.5, timeout / 60),  # Adjust polling based on timeout
+                last_valid_block_height=None,
             )
             return True
         except Exception:
             return False

Or implement manual timeout logic with asyncio.wait_for.

🧰 Tools
🪛 Ruff (0.14.14)

[warning] 470-470: Unused method argument: timeout

(ARG002)


[warning] 478-478: Consider moving this statement to an else block

(TRY300)


[warning] 479-479: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@sniper/trader.py` around lines 470 - 480, The _confirm_transaction method
currently accepts a timeout parameter but never uses it; update the
implementation to enforce the timeout by wrapping the await
self._client.confirm_transaction(...) call with asyncio.wait_for(...,
timeout=timeout) (import asyncio if not present), and keep the existing
try/except behavior (return True on success, return False on
asyncio.TimeoutError or any Exception), or alternatively remove the timeout
parameter and its docstring if you prefer not to support timeouts; reference the
_confirm_transaction method and the self._client.confirm_transaction call when
making the change.

Comment on lines +592 to +598
result = TradeResult(
success=True,
signature=signature,
tokens_amount=min_token_amount,
sol_amount=sol_lamports,
price_per_token=price_per_token,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

tokens_amount reports minimum expected, not actual tokens received.

The TradeResult reports min_token_amount (the slippage-protected minimum) rather than the actual tokens received. This could make PnL calculations inaccurate. Consider querying the token balance after confirmation to get the actual amount.

Suggested approach
             print(f"[TRADER] Buy confirmed: {signature}")

+            # Get actual tokens received
+            actual_tokens = await self.get_token_balance(token)
+
             result = TradeResult(
                 success=True,
                 signature=signature,
-                tokens_amount=min_token_amount,
+                tokens_amount=actual_tokens,
                 sol_amount=sol_lamports,
                 price_per_token=price_per_token,
             )

Note: This adds latency but improves accuracy for position tracking.

🤖 Prompt for AI Agents
In `@sniper/trader.py` around lines 592 - 598, The TradeResult is being populated
with min_token_amount (slippage-protected minimum) instead of the actual tokens
received, which can make PnL inaccurate; after sending and confirming the swap
(you have the signature variable), query the chain for the confirmed transaction
or the token account balance (e.g., via getTransaction/getConfirmedTransaction
or getTokenAccountBalance) to compute the actual received token amount and
assign that value to tokens_amount before constructing the TradeResult; update
the code around the TradeResult creation (where signature, min_token_amount,
sol_lamports, price_per_token are used) to replace min_token_amount with the
observed actual token amount, optionally falling back to min_token_amount if the
on-chain query fails.

@smypmsa smypmsa self-assigned this Feb 2, 2026
@smypmsa
Copy link
Member

smypmsa commented Feb 17, 2026

@rgetmane Thanks for this comprehensive sniper bot implementation! The architecture is solid with good separation of concerns between the Geyser listener, trader, and main orchestration. The Jito bundle integration and multiple exit strategies are particularly well thought out.

However, there are several critical issues that need to be addressed:

Critical issues:

  1. Incomplete file - sniper/trader.py is truncated at line 733 with an incomplete return statement (return Inst). This will cause immediate runtime failure. Please complete the _build_sell_instruction method.

  2. Missing dependencies - The code references files not included in this PR:

  • geyser.generated protobuf files (line 24 in geyser.py)
  • idl/pump_fun_idl.json (multiple references)

Please either include these files or document how to generate/obtain them.

  1. Import path issues - sniper/geyser.py:23 uses sys.path.insert(0, str(Path(__file__).parent.parent / "src")) which assumes a src/ directory, but I don't see this matching the repo structure. Please verify this works or use relative imports.

Important improvements:

  1. Error Handling - Multiple locations have bare except Exception: return None blocks (e.g., geyser.py:99-100, 152-153). This makes debugging nearly impossible in production. Please add logging:
except Exception as e:
print(f"[ERROR] Failed to parse instruction: {e}")
return None
  1. Transaction confirmation - I don't see robust transaction confirmation logic. After submitting via Jito or RPC, you should verify the transaction actually landed on-chain before reporting success.

  2. Testing - For a bot handling real funds, please add at least basic integration tests or a dry-run mode that simulates trades without actually executing them.

Minor suggestions:

  • Consider reducing the blockhash update interval from 5s to 2-3s (line 219 in trader.py) for better reliability
  • Add validation for the private key format before base58 decoding in main.py
  • The shutdown logic (lines 187-208 in main.py) attempts Telegram notifications which could block if the network is down - consider wrapping in a timeout

The core implementation is strong, but these issues (especially 1-3) need to be resolved before this can be safely used. Looking forward to the updates!

@smypmsa smypmsa closed this Feb 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants