Skip to content

nullfox/foxhole

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Foxhole

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.


Contents


What it is

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.

Features

  • 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.

Quick start

# 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 test

The daemon reads secrets from environment variables only — there is no committed .env (it is git-ignored). Never commit private keys.

Known gap: auto-buy is not wired

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.ts CLI.
  • The daemon discovers/notifies (via Codex) and auto-sells any token that lands in the wallet (via the walletChange handler).

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).

Architecture

                          ┌──────────────────────────────┐
   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.ts reads env vars and builds the shared singletons (Solana Connection, wallet Keypair, Codex, pino logger, market/pool caches). A lightweight hand-rolled DI pattern — no framework.
  • Trading engine — bot.ts owns buy(), sell(), swap(), and the waitForSellSignal() position manager, plus trading state (concurrency semaphore, pending-sell set, per-mint stop-loss map).
  • Listeners — listeners.ts + listeners/ wrap Solana account-change subscriptions as raydiumPool / walletChange events.
  • Caches — cache/ in-memory Maps with fetch-through get + 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.

Runtime flow

There are two runtime modes: the always-on daemon and the one-shot CLIs.

The daemon (index.ts)

  1. Constructs the Bot wired to the connection, logger, and caches, and computes the wallet's WSOL ATA (used to pay for buys).
  2. Starts listeners (unless SKIP_BUY is set) and registers a handler for walletChange only — any non-WSOL token balance change becomes a managed position and triggers bot.sell(...). (The raydiumPool event is unhandled — see Known gap.)
  3. Runs the Codex discovery loop every ~3.85s: query → secondary filter → score & sort → desktop-notify newly-seen tokens → print a live table.

Position lifecycle

   (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"

Trading engine

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, runs PoolFilters, enforces concurrency via the semaphore, and retries the WSOL➜token swap up to BUY_RETRIES times.
  • sell() dedupes concurrent sells, loads pool state from the cache (required — the bot can only sell tokens it has pool state for), waits SELL_AUTO_DELAY, then loops waitForSellSignal() → token➜WSOL swap (which also closes the token ATA to reclaim rent).
  • swap() computes min-out under slippage, builds a VersionedTransaction (compute-budget ix + idempotent ATA create on buy + Raydium swap + close on sell), signs, and hands off to DefaultTransactionExecutor.
  • waitForSellSignal() is the exit manager: trailing stop, rug guard, stall exit, and hard TP/SL, polling the Raydium price every 2s.

Safety filters

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.

Discovery & scoring (Codex)

  • services/codex.tsfindTokens(filters) runs a Solana-scoped filterTokens query (createdAt, volume4, liquidity, marketCap); getToken(address) fetches one token. Responses are validated with zod.
  • Secondary filter (index.ts) — mint ends with pump, liquidity/marketCap ≥ 1/35, priceUSD/high24 > 0.3, volume1 > 0.25 × volume4.
  • Scoring (models/token.ts)calculateTokenSortScore (0–10, used by the daemon) and scoreToken (0–100). Both weight volume stability/growth, price change, an upward-trend flag, and a market-cap sweet spot; scoreToken adds holders + liquidity ratio. getTokenOwners reconstructs a token's holder distribution for concentration analysis.

Configuration

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.tsgetWallet 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.

Commands

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.

Testing

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 change

Current coverage focuses on the pure, deterministic logic:

  • models/wallet.test.tsgetWallet correctly 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 getWallet passed a plain number[] to Keypair.fromSecretKey, which requires a Uint8Array in 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 with Uint8Array.from(...).

Project history

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.ts toward the layered context + services + models/pool/filter structure that exists now, moving discovery from purely on-chain pool watching to the Codex API.

File reference

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.tsListeners EventEmitter; 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.tsMarketCache (OpenBook market keys, warm-loadable).
  • src/cache/pool.tsPoolCache (pool state keyed by mint).

Models

  • src/models/liquidity.tscreatePoolKeys.
  • src/models/market.ts — minimal OpenBook market layout + getMinimalMarketV3.
  • src/models/pool.tsPoolFilters orchestrator.
  • src/models/pool/filter.tsFilter implementations.
  • src/models/transaction.tsDefaultTransactionExecutor.
  • src/models/wallet.tsgetWallet multi-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.tspino logger factory.

Types & config

  • src/global.d.ts — ambient Codex result interfaces.
  • tsconfig.json, eslint.config.js, .prettierrc, package.json — tooling.

Caveats & risks

  • The daemon does not auto-buy. Buying is manual/CLI; see Known gap. There is also a TODO in listeners/raydium.ts about switching to onLogs/initialize2 to better isolate genuinely new pools.
  • Small, fixed stake by design. QUOTE_AMOUNT = 0.025 SOL and MAX_TOKENS_AT_ONCE = 3 keep 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. calculateTokenSortScore is 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.

About

Solana execution engine with on-chain safety screening and automated position management — composable safety filters, trailing-stop exits, versioned transactions.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors