Release Notes
What's new in Viper Trading Bot — plain-English notes for every release, newest first.
v1.0.0 — 2026-07-18
Changed
- New Price Impact Guard blocks trades that would execute far below the screen price. Before every EVM trade, the bot now compares the firm quote's effective execution price (price impact plus pool fee) against a fresh on-chain reference and blocks the trade when the gap exceeds your limit. This can stop a trade that previously would have gone through. New controls live in
/settings→ 🔐 Security: 🛡️ Price Impact Guard (on by default; turning it off takes two taps) and 📉 Max Impact (10% by default). The 10% ceiling is set to catch genuinely broken pricing while leaving routine 2–8% thin-pool memecoin buys alone. This is separate from Slippage, which still bounds quote-vs-fill drift and is unchanged. Behavior on a block: manual swaps stop with "Price impact too high"; limit orders and stop-losses are not cancelled — they stay active, keep retrying, and send a one-time "still trying to fill" notice; copy trades and call-channel buys are skipped with a notification. Applies to Base, BSC, Ethereum, Hyperliquid, and Robinhood — not Solana. Note that sells are covered too, so a stop-loss or copy-trade sell on a crashing, illiquid token can be blocked while the price keeps falling; you are always notified, and can still exit manually from/positionsor raise Max Impact. - Perps Quick Long/Short buttons are always visible. The
/perpshub's quick-fire row (keyed to your Quick Ticker setting, BTC by default) now renders whenever you open the hub, directly above Deposit/Withdraw. Before: it appeared only on an empty hub and disappeared as soon as any position was open, on the assumption that Add/Reduce/Close covered it — so opening a second position took extra taps. - Predict
/historywin-rate counts fee-eaten wins correctly. A resolved Predict position now counts as a win whenever its payout is at least its cost, so a position that resolved in your favor but was eaten by fees no longer shows as a loss. Before: wins were classified by the sign of realized PnL, so a barely-profitable outcome that netted negative after fees was miscounted against your win rate. - Predict hub "In positions" shows live market value. The hub's "In positions" figure now reflects the current market value of your open positions — the same number as the hero total up top — with a
(+N unpriced)suffix when some positions can't be priced. Before: it summed cost basis, which could disagree with the hero value. - Bridges from Solana no longer fail to route when a platform fee is set. With a bridge fee configured, a Solana-origin bridge could return no available route at all. Fee parameters are now applied only to EVM-origin quotes, so Solana-origin bridges quote and route normally again; EVM-origin bridges still collect the fee.
- Robinhood Chain (4663) promoted to a full 5th EVM trading chain. Robinhood previously only worked as a bridge destination (Arbitrum Orbit L2, native ETH, USDG (Paxos) instead of USDC since Robinhood has no canonical USDC/USDT). It now trades like Base/BSC/Ethereum/ Hyperliquid: swaps and limit orders execute via 0x Protocol (polling- first, no public WSS yet); limit orders can be paid in USDG as well as ETH;
/bridgenow sends FROM Robinhood too, not just to it; Swap Stables gets a USDG-aware picker (native ↔ USDG only — no USDT leg, no stable-to-stable swap);/trending, wallet create/import/export,/history, and every balances/positions/wallet-list UI surface now include Robinhood. Explorer links use Blockscout. Copy trading and call channels stay deferred to a follow-up (#509). - Jupiter Predict sports events show market-type tabs. Event cards for Polymarket sports events now expose special-markets tabs (moneyline, spreads, totals, and more) instead of just the headline market, so you can pick the exact bet you want directly from
/predict. - GX/Saba special markets ship gated off. Support for Jupiter's GX/Saba special markets lands behind a new
JUPITER_PREDICT_GX_ENABLEDsetting, defaultfalse— it stays off until an orderability probe confirms order builds are signable. Polymarket special markets are unaffected and ship enabled. - Predict buy card shows partial fills and jupUSD refunds. When a Predict order fills for less than the deposit, the pending-open row now reads
≈$X filled (≈$Y back as jupUSD) confirming…instead of just the full deposit. The 0.1% open-fee is now charged on the estimated filled amount (orderCostUsd − estimatedTotalFeeUsdfrom the build response, clamped to never exceed the fee-on-deposit; falls back to the deposit when the estimate is absent) rather than on the whole deposit — so a partially-filled order is never over-charged. - One-tap jupUSD → USDC convert on the Predict hub.
/predictnow shows your jupUSD balance as a sub-row and a💱 Convert jupUSD → USDCbutton (both hidden below $0.01) that jumps straight into the Swap-Stables amount step — jupUSD change left over from partial fills no longer has to be converted by hand. - Solana stable swaps honor your Stablecoin Slippage setting. Bug fix: Swap-Stables on Solana (USDC↔USDT and stable↔SOL) ignored the Stablecoin Slippage setting and used the general Solana slippage default (100 bps) instead of the stablecoin default (25 bps) — running at 4× the intended tolerance. It now passes the stablecoin slippage through, matching the EVM side.
- Low-SOL swaps explain themselves instead of failing blank. When a Solana swap can't be built because the wallet lacks enough SOL to fund the destination token account's rent (e.g. swapping USDC straight into a new token with only cents of SOL left), the bot now surfaces the real reason —
Insufficient SOL — fund the wallet with a bit more SOL.— rather than the genericSwap failed — no response from Jupiter.The build-time compute-unit simulation threads its rejection string up; a funds/rent error aborts before broadcast with the catalog message, while other (non-funds) simulator quirks fall back to a default compute-unit limit and broadcast anyway. - Take-Profit orders now work on Hyperliquid. Copy-trade and call-channel buys on Hyperliquid can create automatic take-profit sell orders, same as Base/Ethereum/BSC. Before: TP orders were silently skipped on Hyperliquid because the allow-list omitted it.
- $5 hard minimum on all bridges. Bridges now enforce a $5 USD floor at confirm time — a sub-$5 bridge is blocked before any quote is fetched with
Minimum bridge size is $5.(the destination-chain screen still shows the soft advisory below $5). When the amount can't be priced in USD the gate fails open and LI.FI's own per-route minimum applies. - LI.FI bridge errors translate at submit. Failed bridge quotes and failed execution now route raw upstream messages through a 9-entry substring catalog (
integrations/lifi_error_catalog.translate_lifi_error) before showing them to the user. Before: cryptic LI.FI strings like "No available routes" or "ChainNotSupportedError" leaked verbatim. Now: users see friendlier copy such as "No bridge route available for this pair." or "Cannot estimate gas fees — destination chain may be congested." Catalog has a 180-day staleness pin (tests/integrations/test_lifi_error_catalog_last_verified.py) so the date is re-verified against LI.FI's evolving message text. Wired atbot/handlers/bridge/submit.py:330(stale-quote refresh) and:463(post-execution terminal failure). - Jupiter Ultra V2 timeout shows actionable text. When Ultra V2
/swap/v2/executetimes out, users now see⚠️ Submission timed out — check /history before retrying.instead of the generic Beam execute-failure message. Backed by Jupiter's non-idempotency contract: a timed-out execute may have landed, retrying with the same signed tx blob would race the pre-confirmation window. The Trigger path catches timeouts the same way internally but has no ContextVar surface — Trigger users see the generic Trigger failure text (intentional asymmetry; Trigger orders are user-managed). - Limit orders that lose their insurance row pause instead of silently broadcasting. When a limit-order's pre-stage
FailedSwapinsert exhausts 3OperationalErrorretries, the executor now pauses the order post-broadcast (before recording) and shows⚠️ Order paused — could not stage recovery row. Tap ▶️ Resume to retry.Before: the broadcast tx had no reconciler insurance row, so a late-confirm would lose the Trade-row record. After: the user can decide to Resume once the on-chain status is known, andsweep_stranded_executing_orders(15-min TTL) is the cascade-failure escape hatch if the pause UPDATE itself fails. - Bot-tracked-only PnL for transferred-in tokens. When a token is surfaced via ➕ Add Token (or any path that produces a synthetic
Traderow —direction="buy",amount_in=0, notx_signature), PnL math now excludes that row from cost basis, Avg Entry, Buys count, and proceeds-cap calculations. Before the fix, a transferred-in token shown alongside one real buy would render Avg Entry weighted against the transferred-in balance (e.g. TIBBIR bug-report: $0.01719 instead of the real ~$0.13) and PnL would display the on-paper move of the *whole* balance (+200,728% instead of the user's actual ~$0 outcome on the tiny real position). New behavior: - Synthetic-only positions render
Avg Entry: N/Aand zero PnL — no inflated stats until the first real buy. - Once a real buy lands, Avg Entry, Buys count, and PnL reflect only bot-executed buys; transferred-in balance is still visible in the Balance line and contributes to the hold-time chip (
⏱️ Xd / Xw) so users still see "when the bot first saw the token". - "Sell 100%" still drains the full on-chain balance (including transferred-in tokens); only the *derived statistics* changed, not the sell mechanics.
- The walk-loop that decides when a position has closed (
_closed_trade_pnland_current_position_tradesinbot/handlers/trading/_helpers/pnl.py) now also skips synthetic rows so a large transferred-in balance can't inflatepeak_tokensand suppress the closed-trade summary on a real-position-closing sell. - BSC limit-order failure-message label:
BNB→BSC. Limit-order failure notifications on BSC now read"BSC limit buy failed: ..."instead of"BNB limit buy failed: ...". Intentional unification with the rest of the user-facing chain label set (copy-trade already used"BSC"). Token-symbol text ("BNB") is unchanged — only the chain name in failure stems flipped. Part of the cross-chain dedup refactor (v9 plan 3) that introduced theEvmChainDisplayfrozen dataclass as the single source of per-chain UX labels. - Liveness hardening for WSS, scheduler ticks, and swap-path errors (v7 Plan 3). Five fault classes were tightened in this release:
- WSS message-flow staleness restarts. Each WSS subscriber (Base NewHeads, Ethereum NewHeads, Base / BSC / Ethereum / Solana wallet monitors, Coinbase native price feed) now stamps
_last_event_aton every received message and exposesis_stale(now, max_silence_s). The 2-minute scheduler health-tick checks each subscriber and restarts any whose feed has gone silent past its threshold (30s for EVM, 60s for Solana / native prices). The newheads stale-restart skips chains with no active orders, mirroring the existing watchdog so an idle chain doesn't cycle through spurious WSS reconnects. - APScheduler tick top-level timeout. Eight scheduler tick callables (4 limit-order ticks,
check_trigger_orders, the inflight- buys / progress-refs sweepers,reconcile_failed_swaps) now wrap their body in_with_top_level_guard(fn, *, name, timeout_s)which combinesasyncio.wait_for(...)with an outertry/except. Without this guard, a wedged HTTP call could wedge the scheduler job indefinitely AND any raised exception would disappear as a silent APScheduler misfire. - Narrow exception classification on the swap path. Five regions now distinguish construction bugs (
ValueError/KeyError/AttributeError/TypeError→ re-raise) from transient errors (httpx.HTTPError/ConnectionError/TimeoutError→ returnNone): EVMswap_base.get_price(newPriceFetchErrorexception for transient errors), EVMtx_helpers(with boundary wrappers inclient_base.pyto preserve thestr | Nonecontract for transfer / withdraw callers), Solanaclient/tx'sclose_empty_accounts_txandburn_and_close_account_tx, Solanajupiter_build'ssimulate_build_cuandassemble_transaction. Solanajito.relay_via_jitokeeps its return-None-on-retry-exhaust semantic (private-mempool relay, no tip — distinct from a bundle relay) and now carries a documenting comment explaining that intent. - Solana RPC retries. Five critical paths now retry on transient HTTP errors via
retry_async+_is_http_retryable:simulate_transaction,simulate_for_cu_limit,get_sol_balance(3 attempts with jitter),get_recent_prioritization_fees,get_latest_blockhash. Threemax_attempts=2sites inclient/token.pywere bumped to 3. - DexScreener 429 cooldown. Both
integrations/dexscreener.pyandchains/native_prices.pynow parseRetry-Afteron a 429, set a module-level monotonic-timestamp gate, and short-circuit subsequent calls until the cooldown expires. Prevents 429 storms from burning through the rate limit. - EVM newheads retry budget.
_WSS_MAX_RETRIESraised from 3 to 5 (delays2 / 4 / 8 / 16 / 30 ≈ 60stotal) so a transient WSS outage at the upstream provider doesn't immediately fall back to polling. _is_blockhash_validis now tri-state (bool | None).True= valid,False= expired (terminal),None= RPC error or empty blockhash. Callers inconfirm_transaction's polling now checkvalid is Falseso an RPC unknown falls through to TIMEOUT instead of being mis-classified as EXPIRED — a potentially-valid tx is no longer lost when the RPC briefly fails.- Notifications are now at-least-once with row-claim dedup. Previously the background workers (limit-order dispatcher, copy-trade executors,
failed_swapsreconciler) could occasionally double-send a fill / "earlier swap recorded" / "we're tracking it" message under concurrent ticks or on retry. They now route throughnotify_once, gated by per-row idempotency timestamps (Order.fill_notified_at,Trade.notified_at,FailedSwap.notified_at) added in migration51a734f6b1e7. The conditional UPDATE acts as the dedup boundary — exactly one tick wins. The winning tick runs the Telegram send; on truthy return the claim is committed, on raise OR onNone/Falsethe claim is rolled back so the next reconciler/scheduler tick can re-drive the same row. Note: callsites without a downstream retry driver (failed-swap-late-confirm) still fire once and lose the message on Telegram outage — same failure mode as before, just expressed via rollback instead of commit-then-lose. Seedocs/DECISIONS.md→ "At-Least-Once Notifications" for details. - Settings handlers no longer clobber concurrent writes. Settings toggles that previously did
set_settings(full_dict) + commit(last-writer-wins) now useupdate_user_settings_atomicper key (SQLjson_set/json_remove). Two handlers writing different keys in the same window will both persist. - State-management hardening (v7). Thirteen findings (4 HIGH, 9 MEDIUM) from a concurrency / shared-state audit:
- TP/SL creation is now an additive upsert. Multiple "buy at $X then take-profit at $Y" actions on the same token now collapse into a single sell order with a summed amount instead of N duplicate active rows. Backed by a partial unique index
uq_order_active_user_wallet_token_kind_triggeron(user_id, wallet_id, token_in, order_type, trigger_price)wherestatus='active' AND side='sell'. Trailing SLs (trigger_price IS NULL) bypass the upsert. BUY-side limit DCA semantics are preserved (the predicate isside='sell'). - Limit-order edit-while-executing race fix. A user editing a limit order at the moment a tick has just claimed it could create a duplicate active row alongside the in-flight execution. The edit handler now captures
cancel_order's return value and surfaces an "Order is currently executing" message instead. - Auto-cancel-confirm now re-fetches native balance. When the user confirms cancelling sell orders to free balance for a buy / withdraw, the handler re-checks
current + freed >= requiredagainst fresh native balance and aborts cleanly if a concurrent buy drained the wallet between prompt and confirm. - EvmWalletMonitor lifecycle lock.
start/stop/watch_wallet/unwatch_wallet/restart_dead_tasksare now serialized under_lifecycle_lockwith cancel-outside-the-lock on stop, mirroringEvmNewHeadsSubscriber. Prevents orphaned monitor tasks if a stop races a concurrent watch / restart. - Solana monitor
(sig, wallet)ring-buffer dedup. A single transaction touching two tracked wallets now dispatches per (sig, wallet) instead of being silently swallowed by sig-only dedup. Ring buffer isdeque(maxlen=4096) + setwith mirror- discard before deque append. All three dispatch paths (HeliustransactionSubscribe, standardlogsSubscribe, HTTP polling fallback) migrated. - Per-row atomic UPDATE for single-column writes.
Userusername / first-name sync, PIN-attempt reset, adminsetfee_command, and 9 call-channel TP/SL setters now useupdate(...).where(...).values(...)instead of ORM-mutate-and-commit. Defensive alignment so a future full-row commit can't clobber a concurrentupdate_user_settings_atomic(or similar atomic write). - Wallet-switch invalidates BOTH old and new balance caches. Previously only the newly-active pubkey was invalidated, so
/positionscould show wallet A's stale balance under wallet B's pubkey for ~30-60s. - Native-price 90s staleness gate. ETH / SOL / BNB getters now serve the cached Coinbase WS price ONLY if its timestamp is within 90s; otherwise they fall through to the existing DexScreener fallback. A wedged WS feed can no longer serve frozen prices for up to 60s.
notify_oncedrops on persistent DB error. Previously a persistent SQLAlchemy error during the conditional UPDATE fell through toawait send_fn(...), risking a duplicate Telegram message. The fallthrough is removed; persistent DB errors now log a warning and returnFalse(re-driveable by a later reconciler tick). Re-sending duplicates is worse UX than dropping.- EVM custom-amount sell — clamp inside lock. "Sell N tokens" now passes the absolute amount through to the swap pipeline; the clamp
min(N, on-chain balance)and the display percentage recompute happen insidewallet_lockagainst the freshly-read balance instead of off a stale UI snapshot. - Burn batch under
wallet_lock._execute_batch_burn's body (re-fetch + build + broadcast + confirm) is now wrapped inwallet_lock(user_id, "solana"). The re-fetch of burnable token accounts is INSIDE the lock so it can't be invalidated by a concurrent buy mid-batch.
Fixed
- Hyperliquid-perps bridge deposits now use the correct USDC decimals. LI.FI returns two USDC tokens for Hyperliquid-perps (chainId 1337); the bot now deterministically selects the 6-decimal perps-deposit USDC regardless of response iteration order, so deposit amounts resolve with the right scale.
- Copy-trade timeout pending text ("⏳ submitted but slow") will no longer fire twice on the same on-chain tx. Migration
d2e8b1c3a701adds the(chain, tx_hash)unique constraint onfailed_swapsso the duplicate-insertIntegrityErrorgate (Solana E-M2 + EVM dual-chain D-F2) actually fires. - The reconciler's "earlier swap has been recorded" message is now routed through
notify_once. Previously theIntegrityErrorbranch fell through tobot.send_message, allowing two parallel ticks to each notify (B-F2).
Performance
wallet_lockfor limit-order execution no longer covers the user-facing fill notification (notify_onceUPDATE + TelegrameditMessageText, which can take seconds under rate-limit). The notification is now deferred to run after the lock releases (D-F3).wallet_lockfor trigger-order fee/recording no longer covers the Jupiter Token API + DexScreener fallback HTTP call. The token-info lookup runs before lock acquisition (D-F4).
v0.1.0 — 2026-07-17
Added
- Initial public release notes page.