A Solana memecoin sniping & auto-trading bot for freshly-launched pump.fun tokens on Raydium.
Foxhole is a small (~2,500 LOC) TypeScript/Node.js command-line trading system. It discovers trending pump.fun tokens that have graduated to a Raydium AMM pool, screens them through on-chain safety filters, and executes buy/sell swaps directly against the Raydium AMM — with positions managed automatically by a take-profit / trailing-stop-loss engine.
⚠️ Financial risk. This software signs and broadcasts real transactions that spend real SOL on highly speculative, newly launched tokens. Memecoin sniping is extremely high-risk. The safety filters reduce but do not eliminate exposure to scams, rugs, and total loss. Use at your own risk and only with funds you can afford to lose.
- What it is
- Features
- Quick start
- Known gap: auto-buy is not wired
- Architecture
- Runtime flow
- Trading engine
- Safety filters
- Discovery & scoring (Codex)
- Configuration
- Commands
- Testing
- Project history
- File reference
- Caveats & risks
Foxhole makes small, fast, speculative SOL-denominated trades on brand-new tokens and exits them automatically according to profit/loss rules. It works across two domains:
- On-chain (Solana / Raydium): subscribes to Solana account-change WebSocket streams to detect new Raydium pools and changes to its own wallet; builds, signs, and submits versioned swap transactions against the Raydium AMM V4 program.
- Off-chain (Codex market data): periodically queries the Codex GraphQL API for tokens matching liquidity/volume/market-cap criteria, scores them with heuristic models, and surfaces the best candidates via desktop notifications and a live console table.
The bot trades a fixed, deliberately small stake per position (0.025 SOL by
default) and caps concurrency to a few simultaneous positions — high-frequency,
small-bet "sniping" rather than large-position trading.
- Pump.fun graduation targeting — discovery filters specifically for mints
ending in
pump, combined with liquidity/volume/market-cap thresholds tuned for tokens that just migrated to Raydium. - On-chain honeypot/rug filters — checks the token is mint-renounced, not freezable, has immutable metadata / socials, optionally has burned LP, and sits within a target pool-size range, before any buy.
- Trailing stop-loss + "stall" exit — ratchets the stop upward as price rises, honors a hard take-profit, and takes the win if the token merely holds a smaller profit (10%) for a sustained window (30s).
- "Don't sell into a rug" guard — if a position drops past a catastrophic threshold (75%), the bot stops trying to sell rather than burning fees.
- Dual token-account detection — wallet monitoring uses both a program-account-change subscription and a log-based re-scan to reliably catch newly created token accounts.
- Heuristic token scoring — two hand-tuned weighted scorers rank candidates by volume stability, volume growth, price trend, and market-cap "sweet spot."
- In-memory market & pool caches — avoid redundant RPC round-trips on the hot path; the market cache can be warm-loaded from all OpenBook markets.
- Flexible wallet import — accepts a raw byte-array secret key, a BIP-39
mnemonic (
m/44'/501'/0'/0'), or a base58 private key, auto-detected. - Standalone CLI tools — manual buy, manual sell, and a filter dry-run.
# 1. Install dependencies (pnpm)
pnpm install
# 2. Provide configuration via a .env / environment (see "Configuration")
export RPC_URL=... # Solana JSON-RPC HTTP endpoint
export WSS_URL=... # Solana WebSocket endpoint
export COMMITMENT=confirmed
export WALLET_PRIVATE_KEY=... # base58, JSON byte-array, or mnemonic
export CODEX_API_KEY=...
# 3. Run the daemon (discovery + notifications + auto-sell)
pnpm start # tsx src/index.ts
# 4. Run the tests
pnpm testThe daemon reads secrets from environment variables only — there is no committed
.env(it is git-ignored). Never commit private keys.
As shipped, the daemon does not buy on its own. index.ts opens a
raydiumPool subscription but intentionally registers no handler for it, so
new-pool events are discarded. The Bot.buy + PoolFilters machinery is fully
built and correct — it is simply not connected to the event.
In practice:
- Buying is a manual action via the
buy.tsCLI. - The daemon discovers/notifies (via Codex) and auto-sells any token
that lands in the wallet (via the
walletChangehandler).
This is deliberate and has history behind it (see Project history):
the initial commit had an on-chain buy handler, but its bot.buy(...) call was
commented out, and the handler was removed entirely when discovery
pivoted to the Codex API. A code comment marks the exact spot in
src/index.ts. To re-enable auto-buy, add a
listeners.on('raydiumPool', ...) handler that decodes the pool state and calls
bot.buy(accountId, poolState).
┌──────────────────────────────┐
entry points │ index.ts buy.ts sell.ts │ check.ts
(executables) └──────────────┬───────────────┘
│ all import
▼
┌─────────────────────┐
shared singletons │ context.ts │ connection, wallet,
│ (composition root) │ codex, logger, caches
└─────────┬───────────┘
│
┌──────────────┬─────────────┼───────────────┬────────────────┐
▼ ▼ ▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ ┌────────────┐
│ bot.ts │ │listeners │ │ caches │ │ services │ │ models │
│ (trading │ │ (Solana │ │ market / │ │ codex / │ │ pool/mkt/ │
│ engine) │ │ subs) │ │ pool │ │ logger │ │ liquidity │
└───────────┘ └──────────┘ └──────────┘ └─────────────┘ │ filters/tx │
│ wallet/tok │
└────────────┘
- Composition root —
context.tsreads env vars and builds the shared singletons (SolanaConnection, walletKeypair,Codex,pinologger, market/pool caches). A lightweight hand-rolled DI pattern — no framework. - Trading engine —
bot.tsownsbuy(),sell(),swap(), and thewaitForSellSignal()position manager, plus trading state (concurrency semaphore, pending-sell set, per-mint stop-loss map). - Listeners —
listeners.ts+listeners/wrap Solana account-change subscriptions asraydiumPool/walletChangeevents. - Caches —
cache/in-memoryMaps with fetch-throughget+save. - Models —
models/pool-key construction, market layout, safety filters, the transaction executor, wallet derivation, holder-distribution & scoring. - Services —
services/the Codex wrapper (zod-validated) and logger.
There are two runtime modes: the always-on daemon and the one-shot CLIs.
- Constructs the
Botwired to the connection, logger, and caches, and computes the wallet's WSOL ATA (used to pay for buys). - Starts listeners (unless
SKIP_BUYis set) and registers a handler forwalletChangeonly — any non-WSOL token balance change becomes a managed position and triggersbot.sell(...). (TheraydiumPoolevent is unhandled — see Known gap.) - Runs the Codex discovery loop every ~3.85s: query → secondary filter → score & sort → desktop-notify newly-seen tokens → print a live table.
(manual) buy.ts ──► Bot.buy(pool)
├─ build Raydium pool keys (pool state + market cache)
├─ run PoolFilters (renounced / freezable / size …)
├─ check Semaphore (≤ 3 concurrent positions)
├─ swap WSOL ➜ token (retry ×3, 20% slippage)
└─ toast "Bought Token"
token in wallet ──► daemon 'walletChange' ──► Bot.sell(token)
├─ dedupe pending sell orders
├─ look up pool (cache) + market keys (cache)
├─ wait SELL_AUTO_DELAY (250 ms)
└─ loop (retry ×3):
waitForSellSignal():
├─ poll Raydium price every 2s (≤ 60 checks)
├─ ratchet trailing stop-loss upward
├─ if down > 75% ➜ abort sell (assume rug)
├─ if held > 10% profit for 30s ➜ sell (stall)
├─ if price > take-profit (40%) ➜ sell
└─ if price < stop-loss (30%/trailing) ➜ sell
swap token ➜ WSOL (10% slippage) + close ATA
└─ toast "Sold Token"
All in src/bot.ts; behavior is controlled by module-level constants:
| Constant | Default | Meaning |
|---|---|---|
COMPUTE_UNIT_PRICE |
421197 |
Priority-fee micro-lamports per CU |
COMPUTE_UNIT_LIMIT |
101337 |
CU cap per transaction |
MAX_TOKENS_AT_ONCE |
3 |
Max concurrent positions (semaphore) |
QUOTE_AMOUNT |
0.025 |
SOL spent per buy |
TAKE_PROFIT |
40 |
Take-profit target, % |
STALL_TAKE_PROFIT |
10 |
"Stall" profit floor, % |
STOP_LOSS |
30 |
Stop-loss distance, % |
TRAILING_STOP_LOSS |
true |
Ratchet the stop up as price rises |
BUY_RETRIES / SELL_RETRIES |
3 / 3 |
Swap submission retries |
BUY_SLIPPAGE / SELL_SLIPPAGE |
20 / 10 |
Slippage tolerance, % |
SELL_AUTO_DELAY |
250 ms |
Delay before starting a sell |
SKIP_SELLING_IF_LOST_MORE_THAN |
75 |
Give-up-on-rug threshold, % |
SELL_SIGNAL_RETRIES |
60 (env) |
Price polls per sell attempt |
buy()resolves market keys + token ATA, builds pool keys, runsPoolFilters, enforces concurrency via the semaphore, and retries the WSOL➜token swap up toBUY_RETRIEStimes.sell()dedupes concurrent sells, loads pool state from the cache (required — the bot can only sell tokens it has pool state for), waitsSELL_AUTO_DELAY, then loopswaitForSellSignal()→ token➜WSOL swap (which also closes the token ATA to reclaim rent).swap()computes min-out under slippage, builds aVersionedTransaction(compute-budget ix + idempotent ATA create on buy + Raydium swap + close on sell), signs, and hands off toDefaultTransactionExecutor.waitForSellSignal()is the exit manager: trailing stop, rug guard, stall exit, and hard TP/SL, polling the Raydium price every 2s.
Orchestrated by PoolFilters (models/pool.ts); implementations in
models/pool/filter.ts. Filters run in parallel; a pool passes only if all
succeed, and each caches its result.
| Filter | Checks | Passes when |
|---|---|---|
RenouncedFreezeFilter |
Mint mintAuthority / freezeAuthority |
Mint renounced and not freezable |
MutableFilter |
Metaplex metadata isMutable + extensions |
Immutable and (optionally) has socials |
BurnFilter |
LP token supply | LP supply is 0 (creator burned LP) |
PoolSizeFilter |
WSOL in the pool's quote vault | Within [min, max] pool size |
Bot.buy currently enables renounced + freezeable and a minPoolSize of
5 WSOL (maxPoolSize: 0 = no max). A near-duplicate LiquiditySizeFilter class
exists but is not wired into the pipeline.
services/codex.ts—findTokens(filters)runs a Solana-scopedfilterTokensquery (createdAt,volume4,liquidity,marketCap);getToken(address)fetches one token. Responses are validated withzod.- Secondary filter (
index.ts) — mint ends withpump,liquidity/marketCap ≥ 1/35,priceUSD/high24 > 0.3,volume1 > 0.25 × volume4. - Scoring (
models/token.ts) —calculateTokenSortScore(0–10, used by the daemon) andscoreToken(0–100). Both weight volume stability/growth, price change, an upward-trend flag, and a market-cap sweet spot;scoreTokenadds holders + liquidity ratio.getTokenOwnersreconstructs a token's holder distribution for concentration analysis.
All secrets/endpoints come from environment variables (read in context.ts and
bot.ts):
| Variable | Used by | Purpose |
|---|---|---|
RPC_URL |
context.ts |
Solana JSON-RPC HTTP endpoint |
WSS_URL |
context.ts |
Solana WebSocket endpoint |
COMMITMENT |
context.ts |
Commitment level (confirmed, finalized, …) |
WALLET_PRIVATE_KEY |
context.ts → getWallet |
Byte-array, mnemonic, or base58 key |
CODEX_API_KEY |
context.ts |
Codex market-data API key |
SKIP_BUY |
index.ts |
If set, the daemon skips starting listeners |
SELL_SIGNAL_RETRIES |
bot.ts |
Overrides the default 60 price polls per sell |
Trading behavior (stake, TP/SL, slippage, concurrency, compute fees) is set via
the constants in bot.ts, not env vars.
| Command | Role |
|---|---|
pnpm start |
Run the daemon (tsx src/index.ts): discovery + notifications + auto-sell. |
pnpm test |
Run the unit test suite (Node's built-in runner via tsx). |
pnpm build |
Compile TypeScript to dist/ (tsc; tests excluded from emit). |
tsx src/buy.ts --token <mint> --pair <pool> |
Manual buy. |
tsx src/sell.ts --token <mint> --pair <pool> |
Manual sell. |
tsx src/check.ts --token <mint> |
Dry-run the discovery filters against one token. |
Tests use Node's built-in test runner (node:test) executed through
tsx (so the project's NodeNext
.js-style imports resolve without a build step). Test files live next to the
code as *.test.ts and are excluded from the tsc build emit.
pnpm test # run once
pnpm test:watch # re-run on changeCurrent coverage focuses on the pure, deterministic logic:
models/wallet.test.ts—getWalletcorrectly imports all three key formats (base58, JSON byte-array, BIP-39 mnemonic) and derives deterministically.models/token.test.ts— the scoring functions are finite/non-negative and reward trend, volume growth, the market-cap sweet spot, and holder count.
Writing these tests surfaced and fixed a real bug: the JSON byte-array branch of
getWalletpassed a plainnumber[]toKeypair.fromSecretKey, which requires aUint8Arrayin web3.js 1.98 and threw"bad secret key size"— so importing a wallet in Solana-CLI array format would have failed. It now wraps the parsed array withUint8Array.from(...).
A scan of the git history shows the project pivoted its discovery strategy:
| Commit | Author | Date | Notable changes |
|---|---|---|---|
9586e30 |
Ben Fox | 2025-01-06 | Initial commit: on-chain sniper with a raydiumPool handler — but bot.buy(...) commented out. |
000bb70 |
Ben Fox | 2025-01-06 | Adds the manual buy.ts / sell.ts CLIs; reworks index.ts. |
54e475f |
Ben Fox | 2025-01-06 | Market layout + sell-flow refinements. |
300b0ef |
Ben Fox | 2025-01-09 | Pivot to Codex discovery: adds codex.ts, check.ts, context.ts, global.d.ts, the pool/filter.ts split, and token scoring; removes the on-chain raydiumPool buy handler from index.ts. |
Takeaways:
- Auto-buy was never live — it was commented out from the first commit and the handler was later removed, which is why the daemon only auto-sells today (see Known gap).
- The architecture matured from a monolithic
pool.ts/index.tstoward the layeredcontext+services+models/pool/filterstructure that exists now, moving discovery from purely on-chain pool watching to the Codex API.
Entry points
src/index.ts— daemon: bot construction,walletChange→sell handler, Codex loop.src/buy.ts/src/sell.ts— one-shot manual buy / sell CLIs.src/check.ts— one-shot filter/eligibility dry-run CLI.
Core
src/context.ts— composition root; shared singletons.src/bot.ts— the trading engine (Bot).
Listeners
src/listeners.ts—ListenersEventEmitter; start/stop subscriptions.src/listeners/raydium.ts— new Raydium AMM V4 WSOL/OpenBook pool subscription.src/listeners/wallet.ts— wallet SPL-token-account subscription (+ log re-scan).
Caches
src/cache/market.ts—MarketCache(OpenBook market keys, warm-loadable).src/cache/pool.ts—PoolCache(pool state keyed by mint).
Models
src/models/liquidity.ts—createPoolKeys.src/models/market.ts— minimal OpenBook market layout +getMinimalMarketV3.src/models/pool.ts—PoolFiltersorchestrator.src/models/pool/filter.ts—Filterimplementations.src/models/transaction.ts—DefaultTransactionExecutor.src/models/wallet.ts—getWalletmulti-format import.src/models/token.ts— holder distribution + scoring. (+token.test.ts)
Services
src/services/codex.ts— Codex market-data wrapper (zod-validated).src/services/logger.ts—pinologger factory.
Types & config
src/global.d.ts— ambient Codex result interfaces.tsconfig.json,eslint.config.js,.prettierrc,package.json— tooling.
- The daemon does not auto-buy. Buying is manual/CLI; see
Known gap. There is also a
TODOinlisteners/raydium.tsabout switching toonLogs/initialize2to better isolate genuinely new pools. - Small, fixed stake by design.
QUOTE_AMOUNT = 0.025SOL andMAX_TOKENS_AT_ONCE = 3keep exposure low; P/L thresholds are percentages of that fixed stake. - Aggressive, opinionated exits. The 75% rug cutoff and 30s stall-exit are pragmatic reactions to memecoin dynamics, not conventional TA.
- Sells depend on cached pool state. A token that appears in the wallet without corresponding cached pool state cannot be auto-sold.
- Hard-coded network & fees.
mainnet-beta; priority fee and compute limit are constants with no dynamic estimation. - Nominal score range.
calculateTokenSortScoreis documented as "0–10" but can exceed 10 (volume sub-scores cap at 2 while the divisor assumes 1); it is only used for relative ranking.