Release Notes
What's new in Viper Trading Bot — plain-English notes for every release, newest first.
v1.21.0 — 2026-08-01
Changed
- The percentage on /positions is now your unrealized profit or loss — the gain or loss on what you still hold. It agrees with the price and avg entry shown right above it, so a token trading below your avg entry always reads red, even if you already took profit earlier. Money you have already taken out now appears on its own
realizedline when you open a position, with atotalline underneath that matches /history.
Fixed
- The largest of your four configured buy amounts now shows a green checkmark when selected in the limit-order builder. Previously it was the one amount that could be selected but never appeared as selected anywhere on screen.
- A custom stop-loss sell percentage now shows on its button instead of leaving all three buttons unmarked.
- Typing a limit-order amount, trigger price, or trailing stop percentage now updates the order card in place instead of posting a second copy below it.
- Switching from a token amount to a percentage in the limit-order builder no longer shows the typed percentage on the wrong button.
v1.20.0 — 2026-08-01
Added
- Tokens whose deepest liquidity sits on a DEX outside a chain's main Uniswap family — a SushiSwap V3 pool, for example — are now priced from that pool directly on Base, Ethereum, BSC and Robinhood. Those pools used to be skipped entirely, so the price came from a third-party listing that could be stale or based on a much shallower pool.
Changed
- ↻ Refresh on /positions now genuinely re-prices. It drops the pool the bot had pinned for each token you hold and looks again, so a token stuck on a drained or stale pool finally updates instead of holding the same figure until the hourly refresh expires. Tokens that still come back without a pool now get one last live price check rather than showing as pending.
- One caveat on that first refresh: a token you transferred in yourself and never traded through the bot can drop off the list for a single render and reappear on the next tap.
- Hitting a rate limit while pricing your positions no longer blocks buying and selling. Price lookups and trade quotes now back off separately, and price lookups are paced to the provider's limit so a large refresh no longer rate-limits itself into showing nothing.
Fixed
- On Robinhood Chain the dollar stablecoin is now labelled USDG (Global Dollar), which is what it has always been. It was previously shown as USDC — a different issuer's asset.
v1.19.0 — 2026-07-31
Changed
- Working out which chain a token is on now gives up after 5 seconds instead of sometimes taking the better part of a minute. If it can't finish in time it asks you to pick a chain rather than guessing — and because the bot handles messages one at a time, a slow lookup no longer holds everyone else up.
Fixed
- The ⬅️ Back button on the stablecoin swap menu now remembers which chain you picked, so returning to a token that's listed on more than one chain reopens its card instead of asking you to choose again. This is the same fix v1.18.0 shipped for the ↻ Refresh button.
v1.18.0 — 2026-07-31
Added
- BSC tokens that trade against a tokenized stock (rather than against BNB or a stablecoin) are now found and priced. Previously these either didn't show up at all or showed the stock's price instead of the token's.
- BSC now reads prices from Uniswap V4 pools, alongside the PancakeSwap pools it already used.
- When a token isn't listed by our main price source, the bot now falls back to a second source instead of giving up. This mostly affects brand-new or thinly-traded tokens.
Changed
- Looking up a token anywhere in the bot — pasting an address, /price, the price refresh button, the limit order builder, and the hide/unhide links — now tells you honestly what happened instead of guessing Base and showing you a wrong card. A token that isn't on any chain gets a not-found message. A token listed on more than one chain asks you to paste the address again so you can pick which one. Only a genuine price-source outage tells you it's worth retrying.
- For a token priced from a stock pair, market cap, FDV and 24h change are now left blank rather than showing the paired stock's figures. The token's own price is unaffected. If the token also trades in a normal pool of its own, those figures are taken from that pool instead of being left blank.
- The ↻ Refresh button on a token card now remembers which chain you picked, so refreshing a token that's listed on more than one chain reopens the card instead of asking you to choose again.
Fixed
- Prices taken from a two-token hop are now cross-checked against the token's deepest direct pool, so a stale or thin intermediate pool can no longer skew the quoted price. This cross-check now also covers tokens found via the second price source, which previously slipped past it.
- Paging through /positions with a token selected no longer blanks out the buttons. The token list, page arrows and navigation stay put.
- Opening the limit-order builder on a token whose chain can't be determined now tells you so, instead of the button doing nothing at all.
- Swapping any token to SOL now always delivers spendable SOL. On the opt-in 0x Solana swap engine the proceeds were arriving as wrapped SOL, which doesn't pay gas and doesn't show as your SOL balance. Jupiter swaps were never affected.
- SOL-funded buys on the 0x Solana swap engine now spend your SOL directly instead of quoting against a wrapped balance you don't hold.
- Solana token prices on /positions now update on every refresh. They could sit unchanged for up to a minute while the market moved.
v1.17.0 — 2026-07-30
Added
- Wrapped SOL now has its own line in your Solana balances, and an ↩️ Unwrap wSOL button appears whenever you're holding some. Unwrapping returns the SOL to your wallet along with the token account's rent.
Fixed
- Wrapped SOL left over from a swap no longer shows up as a position with "cost basis unknown — added manually". It was never a position — it's wrapped native SOL — and it's now shown in your balances instead. Your portfolio total is unchanged; the value just lives in the right place.
- ➕ Add Token now declines native, wrapped-native, and stablecoin addresses, explaining that those are tracked in your balances rather than as positions. Adding one used to create a phantom position with no cost basis.
v1.16.0 — 2026-07-30
Fixed
- Opening a new position on a coin you already held could silently change the leverage of your EXISTING position, since Hyperliquid's leverage setting applies per-coin, not per-order. New positions (and /perps_long, /perps_short) now lock to your existing position's leverage, matching what already happens when you use ➕ Add.
- A prediction-market claim that took a while to confirm and then landed later could lose its "claimed" record and notification if enough time had passed. The claim itself was never at risk — only the bot's own tracking of it — and it's now recovered correctly.
v1.15.0 — 2026-07-28
Fixed
- Sell orders could sell more of a position than you asked for. A 50% limit, stop-loss, take-profit or sell-to-stablecoin order could reach up to 100% of the position when the balance changed between the moment the order was sized and the moment it executed. All four paths now size against the freshest balance.
- Perpetuals could report a Close or Open as successful when the exchange had actually cancelled the order — telling you a position was flat while it was still open. Recovery now reads the order's real status.
- A Solana swap that may have reached the network was reported as a hard failure, so it was never tracked or reconciled. It is now reported as unconfirmed and tracked, matching the other Solana swap path.
- Prediction-market claims could record a permanent $0.00 payout when the real payout was unknown, and auto-claim could get stuck reporting "claim already in progress" forever.
- Burning tokens to reclaim rent could block your other trades on that wallet for as long as the burn confirmations were pending. Burns now only hold the lock while broadcasting, matching every other trade.
- Reclaiming rent from empty token accounts could disrupt a trade running on that wallet at the same time, because the two were never held apart. Claims and trades can no longer overlap.
- A copy-trade sell could stall for up to 15 seconds, holding up every other action on that wallet, when a copy buy for the same token was still going through. The sell now waits before taking the wallet rather than while holding it, so nothing else is blocked.
v1.14.0 — 2026-07-27
Added
- New
/settings → 🔎 Discovered tokensscreen shows how many tokens the bot is tracking on each chain against that chain's limit, with a per-chain Clear all so you can free up space yourself.
Fixed
- Tokens sent to your wallet could stop appearing on a chain once enough very low-value ones had built up, with no way to tell why or clear them. Space now frees up on its own when a chain is full, and the new settings screen lets you clear a chain yourself.
Changed
- Added further safety checks to Solana swap transaction signing: swap route data is now checked against the blockchain before anything is signed. On the rare occasion it can't be checked, the swap stops and asks you to try again instead of going ahead.
v1.13.0 — 2026-07-27
Added
- Tokens sent to your wallet from outside the bot now show up automatically in
/positionson Base, BSC, Ethereum, and Hyperliquid once their value clears a small threshold — no need to add them manually first. Robinhood is not included for now. ➕ Add Token still works for tokens below that threshold, on Robinhood, and for tokens received before this existed.
Changed
- Added further safety checks to Solana swap and cross-chain bridge transaction signing.
v1.12.1 — 2026-07-26
Fixed
- A cross-chain bridge that goes quiet is now followed for a full 24 hours instead of roughly 12 minutes. Transfers that settle slowly used to stop being tracked before they landed, leaving you without a final status message — now you still get one.
- Searching for a token by ticker no longer lets a token with a misleading price reading push a legitimate match out of the results.
- A malformed reply from the token data provider no longer causes a whole batch of token lookups to come back empty — only the bad entry is skipped.
- An extreme liquidity figure from the token data provider no longer errors out ticker search or token lookups; the bad figure is now ignored like any other unusable value.
Changed
- Added further safety checks to Solana transaction signing.
v1.12.0 — 2026-07-25
Added
- New Use 0x Swaps setting for Solana (Settings → Solana), off by default. With it on, your Solana swaps are routed and priced by 0x instead of Jupiter. If 0x cannot produce a usable swap, the bot quietly falls back to Jupiter and your trade goes through as normal — the fallback only ever happens before anything is sent to the chain, so a swap is never submitted twice. Everything else behaves the same: your slippage setting, priority fees, MEV protection and fees all work exactly as they do today, and the fee you pay is identical either way.
v1.11.0 — 2026-07-25
Fixed
- Failure messages now tell you what actually happened. A copy-trade or call-channel buy that times out is reported as still pending instead of "Failed", a cancel on the perps Orders view now says whether it succeeded, may have applied, or was rejected (with the exchange's own reason), and Hyperliquid errors that previously showed a generic "Action failed" now show the real cause. Wording across the bot distinguishes a temporary problem from a permanent one, and messages no longer show two status icons at once.
- Prediction-market fixes: an error left over from an earlier action is no longer shown against your current one; a claim that timed out and then landed now records its payout and sends the 🎉 you were owed; placing an order with no Solana wallet gives a clear message instead of a generic failure; and a broadcast with no confirmation is described honestly as "may or may not have gone through" rather than as a definite failure.
- Bridges and alerts are no longer dropped while still in progress. A bridge whose source transaction has landed but whose destination leg is still settling is no longer reported as "recovered" and removed from tracking. Near-liquidation alerts now work for wallets whose only position is on a builder market, even when a market read fails the first time it is seen.
- Trades and gas fees recorded during a price-feed outage are no longer permanently valued at $0 — the last remaining recovery paths that stored the outage placeholder now record the price as unknown, so your history re-derives it from the current price.
v1.10.0 — 2026-07-24
Added
- Buy an exact number of tokens on Base, Ethereum, BSC, Hyperliquid, and Robinhood with the new "🎯 Buy Tokens" button on the buy card. The bot computes the required native spend via 0x and hard-caps it using your own Price Impact Guard setting (plus your slippage and the platform fee) — never past your balance minus the gas reserve — so a tighter guard means a tighter cap. Before it executes you see the estimated spend, and the receipt shows what was actually spent with an ≈ marker. Tokens with unknown prices, amounts smaller than one whole unit, or transfer taxes are refused up front rather than risking an overpay.
v1.9.0 — 2026-07-24
Fixed
- Trades recorded during a price-feed outage are no longer permanently valued at $0 — recording and daily-stats paths now treat the outage sentinel as "price unknown" and fall back to the current price.
- Token lookups during a market-data outage now show a "price sources are degraded" notice instead of silently assuming the wrong chain (price checks, hide/unhide, limit-order entry, and chart lookups). A token that exists on more than one chain is handled as before and is no longer mislabelled as degraded.
- Hardened outage recovery across the board: wallet monitoring no longer skips blocks when an RPC hides an error inside a 200 response, bridge-recovery tracking survives 0x outages and Telegram send failures, failed-trade records retry through transient database contention, and a Jito rate limit now pauses only the affected region instead of switching MEV protection off everywhere.
v1.8.0 — 2026-07-23
Fixed
- Token prices derived through a two-hop route (a pool quoted in another token, e.g. a tokenized stock) are now checked against the token's direct stable/native pools on every poll — when they diverge by more than 10%, the direct pool's price (where sells actually execute) is served instead. Applies on Base, Ethereum, and Robinhood.
- Two-hop prices no longer understate by the intermediate pool's swap fee (up to ~1% on 1%-fee pools) — the fee is now backed out of the intermediate leg, and pool drift calibration measures against the same corrected value.
v1.7.0 — 2026-07-23
Fixed
- Brand-new tokens whose DexScreener listing still reports zero liquidity (indexing lag) are now probed on-chain instead of showing no price for their first hours — applies on Base, Ethereum, BSC, and Robinhood.
- Fallback prices no longer trust a pair quoted in an unreliable token (e.g. a tokenized stock): when on-chain quoting is unavailable, the price now comes from the deepest stable- or native-quoted pair that holds meaningful liquidity, fixing grossly wrong prices on newly listed tokens.
v1.6.0 — 2026-07-23
Fixed
- When a trade, withdrawal, or bridge broadcast ends in an unknown state (for example a network drop mid-send), the bot no longer reports it as failed — it now tells you the transaction may still land, tracks it, and follows up once the outcome is confirmed on-chain. This covers limit orders, copy trades, call-channel buys, withdrawals, Move Everything, bridges, and stablecoin swaps on every chain.
- Orders recovered from an unknown broadcast now record the real on-chain amounts instead of a zero-amount placeholder, keeping positions and P&L accurate.
- Stablecoin swap failure messages now show the actual failure reason instead of a generic "try again", and a swap that simply hasn't confirmed yet is no longer mislabeled as "reverted".
- Solana withdrawals now show a "broadcast — confirming on-chain" status instead of an unqualified "sent" message, since Solana withdrawals aren't polled for confirmation the way EVM withdrawals are. Check the explorer link to verify it landed.
v1.5.0 — 2026-07-22
Fixed
- On Solana, the platform fee is now charged only after your swap actually confirms — a swap that fails or expires is no longer billed. This applies to copy-trade buys and sells and call-channel buys (manual trades already worked this way).
- Copy-trade sell fees are now calculated from the actual settled proceeds of the sale instead of the pre-trade estimate.
v1.4.0 — 2026-07-22
Fixed
- Tokens whose main liquidity pool is a Uniswap V4 pool quoted in another token (e.g. tokenized-stock pairs like AI/nvda on Robinhood Chain) now price on-chain instead of falling back to the DexScreener price. Pool discovery also handles pairs that list the token on the quote side.
v1.3.0 — 2026-07-20
Fixed
- Call-channel buys now work when a call links the token through a DexScreener chart URL. A DexScreener link points at a trading-pair address, not the token itself, so the bot previously tried to buy the pair address and failed with a "no routes" error. It now resolves the link to the real token and buys that — on both Solana and EVM chains. A message that includes both the chart link and the plain token address still results in a single buy.
v1.2.0 — 2026-07-19
Added
- Bridge transfers to a custom destination address now ask for your PIN first (when you have one set) — same protection withdrawals already had. Preset destinations (your own wallets, your perps account) are unaffected.
Changed
/setpinnow only works in a private chat, never in a group.
Fixed
- Perps transfer confirmations no longer report a false success when a balance read fails mid-transfer, and typed custom HYPE amounts now account for the gas reserve (previously they could fail on-chain after passing the bot's checks).
- Exported private-key messages are now auto-deleted even if the bot restarts within the 15-second delete window.
v1.1.0 — 2026-07-19
Changed
- Robinhood: tokens transferred into your wallet no longer auto-appear in
/positions. The automatic discovery (via the Blockscout API) added several seconds to every multi-chain/positionsload, so it was removed./positionsshows tokens you've traded through the bot again — to track a transferred-in token, add it manually via➕ Add Token. Adding a token on Robinhood also no longer runs a pre-add tradability check, matching the other EVM chains.
Performance
- A cold
/positionsload can no longer be stalled ~30 seconds by a slow token-metadata provider — the render-time metadata fetch now uses the same short single-attempt budget as the price fetch.
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.