feat(sniper): add production-grade pump.fun sniper with Jito bundles#153
feat(sniper): add production-grade pump.fun sniper with Jito bundles#153nightedgeio wants to merge 1 commit intochainstacklabs:mainfrom
Conversation
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>
📝 WalkthroughWalkthroughIntroduces 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
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()
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
|
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. |
There was a problem hiding this comment.
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: ConsiderSKIP_PREFLIGHT=falseas the safer default for production.The
.env.examplesetsSKIP_PREFLIGHT=trueas default. While skipping preflight is faster, it bypasses validation that can catch issues (insufficient balance, invalid accounts) before submission. For a production template,falseis safer—users can opt intotrueafter testing.Suggested change
# Skip preflight checks for faster submission -SKIP_PREFLIGHT=true +SKIP_PREFLIGHT=false
42-46: Consider adding aMIN_SOL_BALANCEparameter.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.1Based 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 ofprint()statements.The module uses
print()throughout (lines 287-288, 312-314, 325, 329, etc.) but coding guidelines require using the centralized logger viafrom 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 Exceptionat line 372 silently returnsNone, 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 NoneSame 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/exceptblock. 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 = Truesniper/main.py (2)
16-21: Use centralized logger instead ofprint()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 useget_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 whenDEBUG=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_directmakes a single attempt and returnsNoneon 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 NoneAs per coding guidelines: "Implement retry mechanisms with exponential backoff for transaction submission/failures."
1-11: Use centralized logger instead ofprint()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: Reuseaiohttp.ClientSessioninstead of creating one per request.Creating a new
ClientSessionpersend()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}")
| 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: |
There was a problem hiding this comment.
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.
| 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}") |
There was a problem hiding this comment.
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).
| # 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 |
There was a problem hiding this comment.
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.
| 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 "" | ||
|
|
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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 FalseOr 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.
| result = TradeResult( | ||
| success=True, | ||
| signature=signature, | ||
| tokens_amount=min_token_amount, | ||
| sol_amount=sol_lamports, | ||
| price_per_token=price_per_token, | ||
| ) |
There was a problem hiding this comment.
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.
|
@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:
Please either include these files or document how to generate/obtain them.
Important improvements:
except Exception as e:
print(f"[ERROR] Failed to parse instruction: {e}")
return None
Minor suggestions:
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! |
Summary
Structure
sniper/main.py- Entry point and bot orchestrationsniper/geyser.py- Geyser gRPC listenersniper/trader.py- Trading logic with Jito supportsniper/.env.example- Configuration templateTest plan
.env.exampleto.envand configurepython sniper/main.py🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes