What This Project Does
Four things happen continuously inside one long-running Rust process:
- Streams every PumpFun (bonding curve) and PumpSwap (AMM) on-chain event via a QuickNode WebSocket subscription.
- Decodes each Anchor event payload and writes a structured row into a local DuckDB file on NVMe.
- 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.
- 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 editconfig.tomlto 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 (logsSubscribeon 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/
├── 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/
├── junny.duckdb # the primary database
├── junny.duckdb.wal # DuckDB write-ahead log
└── events/ # (reserved) raw event logFirst-Time Setup
1. Clone & open a terminal in the project dir
$ cd /Users/hades/0xhades/hades-junny-bot2. Install Rust
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# then restart the shell or: source $HOME/.cargo/env
$ cargo --version3. Create .env from the example
$ 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
$ 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
$ sudo pmset -c sleep 0
# To revert later:
$ sudo pmset -c sleep 206. Build the release binary
$ ./run.sh buildFirst 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.
$ 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
$ ./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.
- In Discord, right-click the channel you want signals in → Edit Channel.
- Left sidebar: Integrations → Create Webhook (or View Webhooks → New Webhook).
- Rename it if you want (the bot overrides display name anyway). Optional avatar.
- Click Copy Webhook URL. The URL looks like
https://discord.com/api/webhooks/<18-digit-id>/<long-token>. - Treat the URL like a password — anyone with it can post as the bot.
- Paste into
.envasDISCORD_WEBHOOK_URL=…. - Run
./run.sh restartso 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:
Defaults (configurable in config.toml under [watchlist]):
min_cluster_kids = 5min_cluster_pnl_sol = 50refresh_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_WALLETSsrc/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.
| Command | What it does |
|---|---|
| ./run.sh start | Build if needed, start the bot in the background with caffeinate to prevent sleep, detach from terminal. |
| ./run.sh stop | Send 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 status | Show PID, uptime, last writer-batch stats, recent errors. |
| ./run.sh logs | tail -f logs/live.log. Ctrl+C exits the tail — it does NOT stop the bot. |
| ./run.sh restart | stop followed by start. ~30–60s downtime. |
| ./run.sh build | Rebuild the release binary (incremental if possible). |
| ./run.sh stats | Briefly 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:
$ ./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 startDirect DuckDB access (read-only, bot stopped)
$ ./run.sh stop
$ duckdb -readonly /Volumes/nvme/hades-junny-bot/junny.duckdb
# or via the stats binary:
$ cargo run --release --bin stats
$ ./run.sh startOptional: 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:
$ 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:
# 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.plistArchitecture
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
DbEventmessages 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)
| Table | Purpose |
|---|---|
| tokens | One row per mint ever observed. Creator wallet, metadata, graduation info. |
| trades | Every buy/sell from pump.fun + PumpSwap. The firehose of ~25+ rows/sec. |
| pools | One row per PumpSwap AMM pool identity. Base/quote mints, creator. |
| wallets | Per-wallet enrichment: first-seen timestamp, funding source, label. |
| funding_edges | Directed 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_migrations | Idempotent version tracker. |
Running process layout
Under ./run.sh start you get exactly one Rust process with these tokio tasks:
- Main task — loads config + env, migrates DB schema, pings RPC, loads initial watchlist, spawns tasks below, waits on Ctrl+C.
- DB writer —
spawn_blockingthread; owns the DB write connection; batches inserts. - PumpFun collector — async; subscribes to PumpFun program logs; checks
CreateEventagainst the watchlist and fires signals. - PumpSwap collector — async; subscribes to PumpSwap program logs, maintains in-memory pool→mint cache.
- Wallet indexer — async; every 30s pulls unindexed wallets, RPC-traces their first funding tx, emits enrichment events.
- Watchlist refresh (Stage 2) — async; every 3600s re-runs the profitable-cluster query and replaces the in-memory creator→operator map.
- Signal consumer (Stage 2) — async; consumes
WatchlistSignalmessages, 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