volangel/core/detector.ts
SOURCE RAW HISTORY

volangel

README.md · v0.1.4 · last touched 2026.04.29

Volangel is a profit-seeking volume bot for Solana launchpad coins. It scans every supported launchpad, detects early-volume signals, enters positions automatically, and exits at coin bond or a configured cumulative-volume threshold. Each volangel runs against a single user wallet. The user keeps custody.

How it runs

The fleet is a long-running event loop. core/scanner.ts ingests new mints across every supported launchpad in real time. core/detector.ts watches each tracked mint for volume spikes that pass the user's strategy threshold. When a signal fires, core/filters.ts validates the candidate against the user's caps (MC range, dev holdings, age). core/executor.ts signs and broadcasts the trade. core/exits.ts watches the open position and triggers the exit when bond or cumulative-volume conditions are met.

Strategies

  • steady · sustained-volume entries, longer holds, higher win rate in chop
  • flash · fast snipe, exits on first volume peak
  • stealth · only enters past a credibility floor, exits at bond
  • heavy · larger positions, exits at bond or cumulative
  • mirror · shadows a target wallet's entries

Sources

24 launchpads supported. See integrations/ for the per-source adapters. Adding a launchpad means writing one adapter that emits NewMint events and handles trade routing.

Build

pnpm install
pnpm dev          # local fleet + site
pnpm build        # next.js production build
pnpm test         # vitest

Deploy

Site deploys to Vercel. Fleet runs as a long-lived Node process. See deploy/vercel.json and deploy/.env.example for the full env list.

1// core/detector.ts — volume-signal detector for the volangel fleet
2// emits an EntryCandidate event when a tracked mint clears the strategy threshold
3
4import { EventEmitter } from 'node:events';
5import type { Mint, VolumeWindow, Strategy, EntryCandidate } from './types';
6import { rollingVolume, recentBuyers } from './windows';
7
8export class Detector extends EventEmitter {
9 private windows = new Map<string, VolumeWindow>();
10
11 constructor(private readonly strategy: Strategy) { super(); }
12
13 /** ingest one trade event from any launchpad adapter */
14 onTrade(mint: Mint, side: 'buy' | 'sell', lamports: number, ts: number) {
15 const w = this.windows.get(mint.address) ?? this.openWindow(mint);
16 w.push({ side, lamports, ts });
17
18 const volSol = rollingVolume(w, this.strategy.windowMs);
19 if (volSol < this.strategy.earlyVolumeMin) return;
20
21 // signal cleared — emit a candidate. filters.ts will run final guards.
22 const candidate: EntryCandidate = {
23 mint,
24 mc: mint.marketCap,
25 volWindowSol: volSol,
26 buyersWindow: recentBuyers(w, this.strategy.windowMs),
27 detectedAt: ts,
28 source: mint.source,
29 };
30 this.emit('candidate', candidate);
31 }
32
33 private openWindow(mint: Mint): VolumeWindow {
34 const w: VolumeWindow = { mint, trades: [] };
35 this.windows.set(mint.address, w);
36 return w;
37 }
38}
1// core/scanner.ts — multi-launchpad event scanner
2// fans out to every adapter under integrations/, normalizes to NewMint and Trade events
3
4import { EventEmitter } from 'node:events';
5import { adapters } from '../integrations';
6import type { SourceId, Mint, Trade } from './types';
7
8export class Scanner extends EventEmitter {
9 private active = new Set<SourceId>();
10
11 async start(sources: SourceId[]) {
12 for (const id of sources) {
13 const adapter = adapters[id];
14 if (!adapter) continue;
15 adapter.on('mint', (m: Mint) => this.emit('mint', m));
16 adapter.on('trade', (t: Trade) => this.emit('trade', t));
17 await adapter.connect();
18 this.active.add(id);
19 }
20 }
21
22 stop() {
23 for (const id of this.active) adapters[id]?.disconnect();
24 this.active.clear();
25 }
26}
1// core/exits.ts — bond + cumulative-volume exit watchers
2// fires the first matching trigger. stop-loss has priority over either.
3
4import type { Position, Strategy, ExitTrigger } from './types';
5
6export function checkExit(p: Position, s: Strategy): ExitTrigger | null {
7 // stop-loss has priority — if the chart is gone, take the L.
8 const dd = (p.entryPrice - p.lastPrice) / p.entryPrice;
9 if (dd >= s.stopLoss) return 'stop_loss';
10
11 // bond — coin migrated to its destination AMM. the early window is over.
12 if (s.exitOn.includes('bond') && p.coin.bonded) return 'bond';
13
14 // cumulative volume — total volume traded through the chart since launch.
15 if (s.exitOn.includes('cumulative') && p.coin.cumulativeVolSol >= s.exitVolSol) {
16 return 'cumulative';
17 }
18
19 return null;
20}
1// strategies/steady.ts — patient template, sustained-volume entries
2
3import type { Strategy } from '../core/types';
4
5export const steady: Strategy = {
6 id: 'steady',
7 earlyVolumeMin: 0.8, // SOL within window
8 windowMs: 60_000,
9 mcMinUsd: 25_000,
10 mcMaxUsd: 200_000,
11 exitOn: ['cumulative'],
12 exitVolSol: 80_000,
13 stopLoss: 0.35,
14 positionSol: 0.20,
15 maxSlippagePct: 6,
16 devHoldingsCapPct: 6,
17 minCoinAgeSec: 45,
18};
1// integrations/pump.ts — pump.fun adapter
2
3import { Adapter } from './adapter';
4import { connection } from '../wallet/rpc';
5
6const PUMP_PROGRAM = '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P';
7
8export const pump = new Adapter({
9 id: 'pump',
10 programId: PUMP_PROGRAM,
11
12 async connect() {
13 this.subscription = connection.onLogs(
14 new PublicKey(PUMP_PROGRAM),
15 this.handleLog,
16 'confirmed',
17 );
18 },
19
20 parseTrade(log: RpcLogs) {
21 // pump.fun emits Buy / Sell instructions; we decode amount + mint from the inner ix
22 const ix = this.decodeInner(log);
23 if (!ix) return null;
24 return { mint: ix.mint, side: ix.side, lamports: ix.lamports, ts: log.blockTime };
25 },
26});
1// file content withheld
2// public-facing docs only show the most-asked-about files.
3// run `pnpm volangel docs --full` from a connected wallet to view the rest.
DOCS V1 Click any file in the tree to view it. detector / scanner / exits / steady / pump / readme have real-looking content. Others show a placeholder.