Services Approach Projects Back to Home
Project · Private & Proprietary

hades-junny-bot

A 24/7 Solana data-collection engine, wallet-forensics bot, and real-time signal generator for the pump.fun + PumpSwap ecosystem. Streams every on-chain event, reconstructs the funding graph between wallets, scores bundler operators by real trading PnL, and fires log + Discord alerts when a creator inside a profitable operator cluster deploys a new token.

Rust 1.89+ DuckDB QuickNode WSS Discord Signals Proprietary Access on Engagement

What This Project Does

Four things happen continuously inside one long-running Rust process:

  1. Streams every PumpFun (bonding curve) and PumpSwap (AMM) on-chain event via a QuickNode WebSocket subscription.
  2. Decodes each Anchor event payload and writes a structured row into a local DuckDB file on NVMe.
  3. Enriches each wallet by tracing its first-ever on-chain transaction to identify the source of funding — building up a directed graph of who-funded-whom.
  4. Fires watchlist signals in real time when a creator wallet belonging to a profitable bundler cluster (≥5 funded burners and ≥+50 SOL lifetime pump.fun PnL by default) deploys a new token. Signals go to the live log AND, optionally, to a Discord webhook.

The wallet-funding graph lets us collapse a bundler operator's many "burner" wallets into one identity and score each operator by their cluster's aggregate PnL. Profitable bundlers' next token launches become real-time signals for downstream trading logic.

Prerequisites

Hardware / OS

  • macOS (tested on Darwin 22.6.0 — should work on any POSIX with minor path tweaks)
  • Mac plugged in at wall power while the bot runs (to prevent sleep)
  • At least 100 GB free on the target storage volume; 1 TB+ recommended for multi-year runs
  • An external NVMe mounted at /Volumes/nvme (or edit config.toml to change paths)

Toolchain

  • Rust 1.89+ (install via rustup)
  • cargo (comes with rustup)
  • git 2.42+

External Services

  • Solana mainnet RPC — shared tier is sufficient for data collection. HTTP endpoint (getTransaction, getSignaturesForAddress, etc.) and WSS endpoint (logsSubscribe on pump.fun + pumpswap programs).
  • Discord webhook (optional) — for Stage 2 signal notifications. Without it, signals still go to the log file.

Project Layout

hades-junny-bot/
hades-junny-bot/ ├── Cargo.toml # Rust manifest (default-run = hades-junny-bot) ├── config.toml # program IDs, collector toggles, RPC pacing, [watchlist] thresholds ├── .env # secrets: QuickNode URLs, DISCORD_WEBHOOK_URL, RUST_LOG (gitignored) ├── .env.example # template for .env ├── run.sh # daemon control (start/stop/status/logs/stats/restart/build) ├── com.hadesbaker.junny-bot.plist # optional launchd agent for auto-start ├── README.md # this file ├── COPY_TARGETS.md # hand-curated list of profitable-trader wallets for future Stage 3 ├── logs/ # live + rotated run logs ├── src/ │ ├── main.rs # entry point — spawns writer, collectors, indexer, watchlist refresh, signal consumer │ ├── config.rs # TOML + env loader (incl. [watchlist] + DISCORD_WEBHOOK_URL) │ ├── signal.rs # Stage 2: consumes WatchlistSignal, logs + optional Discord POST │ ├── watchlist.rs # Stage 2: creator→operator map, hourly DB refresh, CEX blocklist │ ├── db/ │ │ ├── mod.rs # connect_and_migrate, open_primary, pool-cache loader │ │ ├── events.rs # DbEvent enum (messages to the writer task) │ │ ├── writer.rs # the one write-capable DB task; batches inserts │ │ └── schema.sql # 8 tables, idempotent │ ├── decode/ │ │ ├── pumpfun.rs # Anchor event decoder for the bonding curve │ │ └── pumpswap.rs # Anchor event decoder for the AMM │ ├── ingest/ │ │ ├── pumpfun.rs # WSS subscription + decode loop (bonding curve) + watchlist hook │ │ └── pumpswap.rs # WSS subscription + decode loop (AMM) │ ├── index/ │ │ └── wallets.rs # background task — resolves funders for new wallets │ └── bin/ │ ├── stats.rs # read-only DB stats CLI (funders, clusters, creators, signal back-test, strategy sim) │ ├── pumpswap_backfill.rs # one-shot: indexes all existing PumpSwap pools │ └── wallet_indexer.rs # standalone indexer (only useful when bot is stopped)

Data lives outside the repo at /Volumes/nvme/hades-junny-bot/:

/Volumes/nvme/hades-junny-bot/
/Volumes/nvme/hades-junny-bot/ ├── junny.duckdb # the primary database ├── junny.duckdb.wal # DuckDB write-ahead log └── events/ # (reserved) raw event log

First-Time Setup

1. Clone & open a terminal in the project dir

~/setup
$ cd /Users/hades/0xhades/hades-junny-bot

2. Install Rust

~/rust
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # then restart the shell or: source $HOME/.cargo/env $ cargo --version

3. Create .env from the example

.env
$ cp .env.example .env # Edit .env with your editor; set: QUICKNODE_RPC_HTTP_URL=https://your-subdomain.solana-mainnet.quiknode.pro/YOUR_TOKEN/ QUICKNODE_RPC_WSS_URL=wss://your-subdomain.solana-mainnet.quiknode.pro/YOUR_TOKEN/ RUST_LOG=info,hades_junny_bot=debug # OPTIONAL — Discord signals: DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/.../...

4. Verify the NVMe volume is mounted

~/verify
$ ls /Volumes/nvme/ $ df -h /Volumes/nvme

If your storage lives somewhere else, edit config.toml and change db_path and event_log_dir.

5. Disable sleep on wall power

~/pmset
$ sudo pmset -c sleep 0 # To revert later: $ sudo pmset -c sleep 20

6. Build the release binary

~/build
$ ./run.sh build

First build takes ~2 minutes. Incremental rebuilds after code changes are ~5–30 seconds.

7. One-time: backfill PumpSwap pools

Captures every pool that already exists on-chain so the pumpswap collector can map incoming trades to their base mint from moment one.

~/backfill
$ cargo run --release --bin pumpswap_backfill

Runtime: ~5–10 minutes. Inserts ~50,000 rows into the pools table. You only need to do this once, ever.

8. Start the bot

~/start
$ ./run.sh start

That's it. Close the terminal if you want — the bot is detached under caffeinate and keeps running.

Discord Webhook Setup

Stage 2 signals are always written to logs/live.log. A Discord webhook mirrors each signal to a channel so you can watch them accrue in real time.

  1. In Discord, right-click the channel you want signals in → Edit Channel.
  2. Left sidebar: IntegrationsCreate Webhook (or View WebhooksNew Webhook).
  3. Rename it if you want (the bot overrides display name anyway). Optional avatar.
  4. Click Copy Webhook URL. The URL looks like https://discord.com/api/webhooks/<18-digit-id>/<long-token>.
  5. Treat the URL like a password — anyone with it can post as the bot.
  6. Paste into .env as DISCORD_WEBHOOK_URL=….
  7. Run ./run.sh restart so the bot picks up the new env var.

The bot logs spawning watchlist signal consumer discord=true at startup when the webhook is configured. Signals arrive in Discord as a yellow embed with token name & symbol, mint, creator wallet, cluster operator, slot, timestamp, and links to pump.fun + Solscan.

Stage 2: Watchlist + Signals

How it works

On startup and once per hour, the bot runs this SQL against the local DB:

"Find every creator wallet whose funder runs a cluster of ≥N burner wallets with ≥+M SOL lifetime PnL on pump.fun trades only."

Defaults (configurable in config.toml under [watchlist]):

  • min_cluster_kids = 5
  • min_cluster_pnl_sol = 50
  • refresh_interval_secs = 3600

The result is an in-memory HashMap<creator_wallet, operator_wallet>. On every pump.fun CreateEvent the bot sees, it checks whether the creator is in that map. If yes, it fires a WatchlistSignal through an async channel; the signal consumer logs it and optionally posts to Discord.

PumpSwap trades are excluded from the PnL calculation to avoid a known decoder bug that inflates sol_amount for pools whose quote mint isn't WSOL. pump.fun trades are always SOL-denominated and clean.

CEX / service hot-wallet blocklist

Some wallets that look like bundler operators in the raw graph are actually centralized-exchange hot wallets with hundreds of unrelated users downstream. These produce noisy false-positive signals. The project maintains a hardcoded blocklist in two places that must stay in sync:

  • src/watchlist.rs::CEX_AND_SERVICE_WALLETS
  • src/bin/stats.rs::CEX_AND_SERVICE_WALLETS

The stats binary also auto-flags suspicious funders with a max/min funding-amount ratio > 500× as [suspicious — verify on Solscan]. Real bundlers fund their burners in a narrow range (usually 2–30×); wild variance strongly suggests mixed-user service traffic.

When no signals are firing

If ./run.sh status shows RUNNING but you never see signal log lines or Discord notifications, check the initial watchlist loaded line on startup. If it shows creators = 0 operators = 0, no operators currently meet the threshold — either the bot hasn't been running long enough, or the thresholds are too strict for your dataset. Loosen min_cluster_pnl_sol temporarily to confirm the pipeline works.

Day-to-Day Operations

All runtime control goes through ./run.sh.

CommandWhat it does
./run.sh startBuild if needed, start the bot in the background with caffeinate to prevent sleep, detach from terminal.
./run.sh stopSend SIGINT for a graceful shutdown. Waits up to 60s for the writer to drain, then escalates to SIGTERM/SIGKILL if the process refuses to exit.
./run.sh statusShow PID, uptime, last writer-batch stats, recent errors.
./run.sh logstail -f logs/live.log. Ctrl+C exits the tail — it does NOT stop the bot.
./run.sh restartstop followed by start. ~30–60s downtime.
./run.sh buildRebuild the release binary (incremental if possible).
./run.sh statsBriefly stops the bot, runs the stats binary (DuckDB locks prevent concurrent external readers), then restarts. ~20–30s downtime.

Other Useful CLI Commands

These don't go through run.sh — they're direct binary invocations.

cargo run --bin stats — offline DB inspection

Opens the DuckDB file in read-only mode. Will fail with a lock error while the bot is running because DuckDB holds cross-process exclusive locks even against readers. Use ./run.sh stats instead, which handles the stop/start dance.

cargo run --bin pumpswap_backfill — one-shot pool catalog

Queries every existing PumpSwap pool via getProgramAccounts and loads them into the pools table. Run once at initial setup. Bot must be stopped.

cargo run --bin wallet_indexer — manual wallet indexing

Standalone version of the wallet-indexer logic. Only useful when the bot is stopped. The main bot runs a continuous in-process version of this — you shouldn't need the standalone except for occasional retry passes with a higher pagination cap:

~/retry-pass
$ ./run.sh stop $ WALLET_INDEXER_RETRY_LABEL=indexing_incomplete \ WALLET_INDEXER_MAX_PAGES=20 \ WALLET_INDEXER_BATCH=100 \ cargo run --release --bin wallet_indexer $ ./run.sh start

Direct DuckDB access (read-only, bot stopped)

~/duckdb
$ ./run.sh stop $ duckdb -readonly /Volumes/nvme/hades-junny-bot/junny.duckdb # or via the stats binary: $ cargo run --release --bin stats $ ./run.sh start

Optional: Launchd Auto-Start on Mac Login

For a truly set-and-forget setup, install the launchd plist so the bot starts automatically when you log in AND restarts itself on crash:

~/launchd
$ cp com.hadesbaker.junny-bot.plist ~/Library/LaunchAgents/ $ launchctl load ~/Library/LaunchAgents/com.hadesbaker.junny-bot.plist

Once loaded, do not use ./run.sh start/stop — launchd will fight you. Instead:

~/launchd-control
# Graceful stop (leaves it stopped until reboot): $ launchctl kill INT system/com.hadesbaker.junny-bot # Fully disable auto-start + stop: $ launchctl unload ~/Library/LaunchAgents/com.hadesbaker.junny-bot.plist

Architecture

Data flow

data flow
QuickNode WSS ─┐ ├─► pumpfun collector ─┬─► DbEvent channel ─► writer task ─► DuckDB └─► pumpswap collector ─┘ (batched, 500/tx) │ │ Event::Create hits watchlist? ▼ ┌─ WatchlistSignal channel ─► signal consumer ─┬─► log (always) │ └─► Discord webhook (optional) │ watchlist ◄── refreshes hourly from DB (read conn cloned from primary) ▲ │ QuickNode HTTP ▲ │ wallet indexer (bg task) ──► DbEvent channel (same writer) (every 30s, rate-limited)
  • One DB writer task owns the only write-capable connection. All other tasks send DbEvent messages via an async channel.
  • Collectors, indexer, watchlist refresh, and signal consumer operate independently and can fail/reconnect without affecting each other.
  • The wallet indexer and watchlist refresher use cloned read connections that share the same in-process DB instance, so they can run SELECTs alongside writes without lock contention.
  • Inserts are batched in transactions of up to 500 events for ~10× speedup over per-row auto-commit.
  • Signal emission is non-blocking (try_send) so the decoder's hot path never stalls.

Database schema (DuckDB)

TablePurpose
tokensOne row per mint ever observed. Creator wallet, metadata, graduation info.
tradesEvery buy/sell from pump.fun + PumpSwap. The firehose of ~25+ rows/sec.
poolsOne row per PumpSwap AMM pool identity. Base/quote mints, creator.
walletsPer-wallet enrichment: first-seen timestamp, funding source, label.
funding_edgesDirected SOL-transfer edges — the wallet graph.
pool_states(Reserved) time-series pool reserves for price trajectory analysis.
bonding_curve_states(Reserved) time-series bonding curve reserves.
schema_migrationsIdempotent version tracker.

Running process layout

Under ./run.sh start you get exactly one Rust process with these tokio tasks:

  1. Main task — loads config + env, migrates DB schema, pings RPC, loads initial watchlist, spawns tasks below, waits on Ctrl+C.
  2. DB writerspawn_blocking thread; owns the DB write connection; batches inserts.
  3. PumpFun collector — async; subscribes to PumpFun program logs; checks CreateEvent against the watchlist and fires signals.
  4. PumpSwap collector — async; subscribes to PumpSwap program logs, maintains in-memory pool→mint cache.
  5. Wallet indexer — async; every 30s pulls unindexed wallets, RPC-traces their first funding tx, emits enrichment events.
  6. Watchlist refresh (Stage 2) — async; every 3600s re-runs the profitable-cluster query and replaces the in-memory creator→operator map.
  7. Signal consumer (Stage 2) — async; consumes WatchlistSignal messages, logs at INFO with 🚨 marker, POSTs Discord embed if webhook configured.

Troubleshooting

"Already running (PID …)" when you try to start

Another instance (or a stale launchd daemon) has the pidfile. Check with ./run.sh status. If the PID isn't actually alive: rm .run.pid then ./run.sh start.

"IO error: Could not set lock on file" when running stats

The bot is running and holds the DuckDB write lock. Use ./run.sh stats instead (it pauses + resumes), or ./run.sh stop first.

Bot exits immediately after ./run.sh start

Check logs/live.log — most likely .env is missing or QUICKNODE_RPC_HTTP_URL is wrong. The bot fails fast on startup if it can't reach the RPC.

QuickNode rate-limit errors

The shared tier is generous but not infinite. If you see sustained rate-limit messages, either wait and let the internal backoff absorb it, or upgrade to a dedicated endpoint. Typical sustained load: ~30 req/s across all tasks.

Log file getting huge

./run.sh start rotates the previous live.log to live-YYYYMMDD-HHMMSS.log. Old rotations accumulate under logs/ — clean them up manually as needed.

DuckDB file is corrupted

Rare but possible after a hard crash. Restore from the most recent .wal-sibling or a manual backup. Pump the pool backfill back in if the pools table was lost.

Mac went to sleep anyway

Make sure sudo pmset -c sleep 0 is in effect: pmset -g | grep sleep. Close the lid only if your Mac is set to not sleep on lid close with an external display.

No Stage 2 signals firing

See "When no signals are firing" in the Stage 2 section above. Most common cause: the dataset hasn't accumulated enough trades for any operator to cross the min_cluster_pnl_sol threshold yet.

Discord webhook posts failing

Check the log for Discord webhook post failed lines — they include the HTTP status and body. Common causes: webhook URL revoked or channel deleted (recreate + restart), or rate-limited (HTTP 429 — Discord allows ~30 messages/minute per webhook).

Interested in hades-junny-bot?

This system is proprietary and not open-source. Access is granted on a case-by-case basis through our standard engagement process. If you'd like to evaluate, license, or build on top of hades-junny-bot — or commission a custom Stage 3 trading layer on its signal output — reach out through the Request Engagement feature.

Request Engagement