Echo · Technical Reference

Documentation

Everything you need to understand, operate, and extend the ECHO airdrop engine — from the drop cycle internals to every environment variable.

01 — Overview

What is ECHO

ECHO is a Solana on-chain airdrop protocol built around a simple loop: creator fees accumulate in an engine wallet, the radar finds the hottest trending coin, Jupiter buys it with the fee pot, and the bag is airdropped pro-rata to every qualifying $ECHO holder — automatically, every 30 minutes, with no claiming step.

The protocol is fully transparent. Every swap and airdrop is a public Solana transaction. The engine wallet address is published on the site. Holders can verify every drop on Solscan.

Stack

Runtime
Node.js ≥ 22 (ES Modules)
Uses node:sqlite (native, no ORM) and node:crypto for ed25519 + PDA math. Zero required dependencies — @solana/web3.js and bs58 are optional and only needed for live swap/airdrop execution.
Chain
Solana mainnet via Helius RPC
Holder indexing uses Helius's getTokenAccounts DAS method (paginated, up to 100k accounts). Swap execution uses Jupiter v6 quote + swap APIs. ATA creation and SPL transfers are built with raw instructions.
Storage
SQLite (WAL mode)
Two tables: holders (live balance snapshot) and drops (immutable drop log). A meta k/v table stores state across restarts. No external database required.
Server
Plain Node.js HTTP (no framework)
Two JSON endpoints (/api/state, /api/drops) and static file serving for public/. The frontend polls every 4 seconds — no WebSockets needed.
02 — Architecture

Architecture

The codebase is intentionally flat — one process, no build step, no framework. Each module has a single responsibility.

src/ index.js # entry — validates config, wires modules, starts everything config.js # all env var parsing — single source of truth for settings db.js # SQLite init, schema, transaction helper, meta k/v engine.js # drop cycle — fee claim → radar → Jupiter swap → airdrop indexer.js # Helius holder polling → SQLite, recipient selection trending.js # radar — DexScreener, pump.fun KOTH, manual override solnative.js # zero-dep base58, ed25519 keypairs, PDA math, RPC calls pda.js # pump.fun bonding-curve address derivation server.js # HTTP server — /api/state, /api/drops, static files bot.js # optional X/Twitter post on every drop public/ index.html # full frontend — CSS + vanilla JS, polls /api/* docs.html # this page scripts/ keygen.js # generate a fresh Solana keypair (zero deps) prelaunch.js # derive bonding curve address + print env checklist data/echo.db # SQLite file — created at first run, gitignored

Module dependencies

index.js imports engine, indexer, and server, each of which imports config and db. engine additionally imports trending, solnative, pda, and bot. There are no circular deps.

No build step. Everything runs directly with node src/index.js. Node 22+ is required for the node:sqlite built-in. The optional packages (@solana/web3.js, bs58) are dynamically imported at runtime only when a live swap or airdrop is about to execute.

03 — How it works

Drop cycle

The engine runs a tick() every 5 seconds. When Date.now() ≥ nextDropAt, runDrop() fires. The cycle repeats on a fixed interval (DROP_INTERVAL_MIN, default 30 min).

01
Fee claim optional
If AUTO_CLAIM=1, the engine sends a request to pumpportal.fun to collect outstanding creator fees into the engine wallet before calculating the balance. Skipped in dry-run or mock mode.
02
Balance check
Fetches the engine wallet's SOL balance via RPC. Computes spendable = balance − RESERVE_SOL. dropLamports = spendable × SPLIT_DROP, then capped at MAX_DROP_SOL. If dropLamports < MIN_DROP_SOL, the cycle is logged as skipped and fees roll over to the next cycle.
03
Radar — pick the trending coin
Calls pickTrending(). In auto mode, tries DexScreener top-boosted Solana tokens first, then falls back to pump.fun king-of-the-hill. A manual override bypasses both. See Trending radar.
04
Recipient selection
Calls getRecipients() from the indexer: filters holders with balance ≥ MIN_HOLD_TOKENS, removes EXCLUDED_WALLETS, sorts by balance descending, takes top MAX_RECIPIENTS. If the list is empty, the drop is logged as skipped (silent — no console output).
05
Jupiter swap — SOL → trending coin
Gets a quote from Jupiter v6, deserializes and signs the returned VersionedTransaction, sends and confirms. The output token amount and decimals are read from the quote response. Slippage tolerance is SLIPPAGE_BPS (default 500 = 5%).
06
SPL airdrop — distribute to holders
Pro-rata by balance. Batched 6 recipients per transaction (ATA create-idempotent + transferChecked per recipient). Each batch is sent and confirmed before the next. See SPL airdrop.
07
Rake transfer
Sends spendable × SPLIT_RAKE SOL to RAKE_WALLET via a raw SystemProgram transfer.
08
Log + announce
Inserts a row into drops with status done. If Twitter keys are configured, bot.js posts a drop announcement including the Solscan swap link.

Budget math

balance       = engine wallet SOL balance
spendable     = max(0, balance − RESERVE_SOL)
dropLamports  = floor(spendable × SPLIT_DROP)
dropLamports  = min(dropLamports, MAX_DROP_SOL × 10⁹)   // per-drop cap
rakeLamports  = floor(spendable × SPLIT_RAKE)

  → SPLIT_DROP + SPLIT_RAKE must be ≤ 1.0
  → excess above MAX_DROP_SOL rolls over to the next cycle
04 — Trending radar

Trending radar

Implemented in src/trending.js. Source priority is controlled by TRENDING_SOURCE.

auto
DexScreener — top boosted (primary)
Fetches https://api.dexscreener.com/token-boosts/top/v1. Takes the first Solana token (chainId === 'solana') with a valid tokenAddress. Reason recorded: "top boosted on dexscreener".
auto
pump.fun king-of-the-hill (fallback)
Fetches https://frontend-api.pump.fun/coins/king-of-the-hill?includeNsfw=false. Used when DexScreener fails or returns no Solana token. Reason recorded: "pump.fun king of the hill".
manual
Operator override — always wins
Set TRENDING_SOURCE=manual + MANUAL_TARGET_MINT + optionally MANUAL_TARGET_SYMBOL. Bypasses all API calls. Your safety valve if either external API changes or goes down.
mock
Simulated rotation
Cycles through 8 fake coin names + randomly generated fake mint addresses. Used when MOCK=1.

API fragility. The DexScreener and pump.fun endpoints are unofficial and can change without notice. Always have MANUAL_TARGET_MINT ready as a fallback. If both APIs fail and no manual target is set, the drop is logged as failed.

05 — Holder indexer

Holder indexer

Implemented in src/indexer.js. Polls Helius for every token account under TOKEN_MINT and maintains a live snapshot in the holders SQLite table.

Helius getTokenAccounts

Uses Helius's proprietary DAS RPC method (not a standard Solana method). Paginates with a cursor field, up to 100 pages × 1000 accounts = 100k holders maximum. Multiple accounts owned by the same wallet are summed. Wallets with zero balance after summing are deleted from the table.

POST https://mainnet.helius-rpc.com/?api-key=<KEY>
{
  "jsonrpc": "2.0",
  "method": "getTokenAccounts",
  "params": { "mint": "<TOKEN_MINT>", "limit": 1000, "cursor": "<prev>" }
}

Recipient selection

getRecipients() applies three filters in order:

  • Dust filterbalance < MIN_HOLD_TOKENS — removes wallets holding fewer than the minimum token amount. Default: 100,000 tokens.
  • Exclusion list — removes all addresses in EXCLUDED_WALLETS. Critical: the pump.fun bonding curve holds a large fraction of supply and must always be excluded, or nearly the entire airdrop goes to the curve and is lost.
  • Top-N cap — sorts by balance descending and takes the top MAX_RECIPIENTS wallets. Default: 150. Each recipient requires an ATA creation instruction (~0.002 SOL rent), so the cap exists for economic reasons.

Poll interval is controlled by POLL_INTERVAL_SEC (default 30 s). On the first poll after a fresh start, the table is empty — drops fire as skipped until the first poll completes.

06 — SPL airdrop

SPL airdrop mechanism

Implemented in Engine.distribute() inside src/engine.js. Executes entirely with raw Solana instructions — no SPL helper libraries required.

Per-recipient share

totalBal = sum of all recipient.balance values
share_i  = floor(totalRaw × (balance_i / totalBal))
  where totalRaw = floor(tokensBought × 10^decimals)

Rounding is floor, so the engine wallet keeps any dust remainder.

Transaction structure

Each transaction handles 6 recipients. For each recipient, two instructions are packed:

ix 1
ATA create-idempotent
Instruction discriminator 0x01 on the Associated Token Account program. Creates the recipient's ATA if it does not exist; no-ops if it already exists. The engine wallet pays the rent (~0.002 SOL per new account).
ix 2
SPL transferChecked
Instruction discriminator 0x0C (12) on the Token program. Transfers exactly share_i raw token units from the engine's source ATA to the recipient's ATA, with a decimals check as a safety guard.

ATA rent is real money. Creating a new ATA costs ~0.002039 SOL. With MAX_RECIPIENTS=150 and all recipients being new wallets, worst-case ATA overhead is ~0.31 SOL per drop. Keep RESERVE_SOL high enough to cover this in addition to transaction fees.

07 — Reference

Configuration

All configuration is via environment variables. Parsed in src/config.js. No config files — change a var and restart.

Chain required in live mode

VariableDefaultDescription
HELIUS_API_KEYHelius API key. Used to build the RPC URL unless RPC_URL is set. Free tier at helius.dev.
RPC_URLOverride the Helius RPC URL with any Solana RPC endpoint.
TOKEN_MINTThe $ECHO token mint address. All holder polling and CA display is driven by this.
ENGINE_WALLET_SECRETBase58 private key of the engine wallet. Must be the pump.fun creator wallet. Required when DRY_RUN=0.
ENGINE_WALLET_ADDRESSPublic address of the engine wallet. Used in read-only mode when the secret is not set.
RAKE_WALLETSOL destination for the rake (15% of spendable by default). If unset, rake stays in engine wallet.
EXCLUDED_WALLETSComma-separated list of wallet addresses to exclude from airdrops. Must include the bonding curve address and the engine wallet.

Modes

VariableDefaultDescription
MOCKfalseSimulate everything. Fake holders, fake fees, fake drops. No chain calls. Safe for UI testing.
DRY_RUNtrueLive chain reads (balance, holders, trending) but no transactions. Drops are logged with swap_sig = DRY_RUN. Flip to 0 for real execution.

Drop cycle

VariableDefaultDescription
DROP_INTERVAL_MIN30Minutes between drop cycles.
MIN_DROP_SOL0.05Minimum spendable SOL to trigger a drop. Below this, fees roll over.
MAX_DROP_SOL0.75Maximum SOL per drop. Excess above this rolls over to the next cycle.
RESERVE_SOL0.1Always-kept SOL buffer. Covers ATA rent + tx fees. Never spent on swaps.
SPLIT_DROP0.85Fraction of spendable SOL that buys the trending coin. Must satisfy SPLIT_DROP + SPLIT_RAKE ≤ 1.
SPLIT_RAKE0.15Fraction of spendable SOL sent to RAKE_WALLET.
SLIPPAGE_BPS500Jupiter swap slippage tolerance in basis points (500 = 5%).
AUTO_CLAIMfalseAutomatically claim pump.fun creator fees before each drop via pumpportal.fun.

Recipient eligibility

VariableDefaultDescription
MIN_HOLD_TOKENS100000Minimum $ECHO balance (UI units) to be eligible. Dust filter. Disclose this publicly.
MAX_RECIPIENTS150Maximum holders paid per drop (top by balance). ATA rent makes unlimited recipients uneconomical.

Trending source

VariableDefaultDescription
TRENDING_SOURCEautoauto tries DexScreener then pump.fun KOTH. manual always uses MANUAL_TARGET_MINT.
MANUAL_TARGET_MINTMint address to always buy when TRENDING_SOURCE=manual. Also acts as safety fallback if set alongside auto.
MANUAL_TARGET_SYMBOLTARGETDisplay symbol for the manual target.

Infrastructure

VariableDefaultDescription
PORT3000HTTP server port.
DB_PATH./data/echo.dbSQLite file path. Created automatically on first run.
POLL_INTERVAL_SEC30How often the indexer polls Helius for updated holder balances.

Twitter / X bot optional

VariableDefaultDescription
TWITTER_APP_KEYAll four must be set to enable auto-posting. Obtain from the X developer portal with read+write permissions. If any are missing, drops are only logged locally.
TWITTER_APP_SECRET
TWITTER_ACCESS_TOKEN
TWITTER_ACCESS_SECRET

Mock / demo mode

VariableDefaultDescription
MOCK_FEE_SOL_PER_MIN0.12Simulated fee accrual rate (SOL/min) in mock mode.
MOCK_DROP_INTERVAL_SEC40Drop cycle interval in mock mode (seconds, not minutes).
MOCK_HOLDER_COUNT140Number of synthetic holders generated in mock mode.
08 — API

HTTP API

Two read-only JSON endpoints. The frontend polls both every 4 seconds. No authentication.

GET /api/state

Engine status, live stats, and branding. Polled every 4 s by the frontend.

{
  "brand":          { "name", "ticker", "tagline", "buyUrl" },
  "ca":             string,          // TOKEN_MINT — drives Copy CA
  "mock":           boolean,
  "dryRun":         boolean,
  "now":            number,          // unix seconds
  "nextDropAt":     number,          // unix seconds
  "cycleSec":       number,          // DROP_INTERVAL_MIN × 60
  "current":        { "mint", "symbol", "name", "reason" } | null,
  "tankSol":        number,          // spendable SOL (balance − RESERVE_SOL)
  "totalSol":       number,          // sum of sol_spent across all done drops
  "dropsDone":      number,
  "distinctCoins":  number,
  "holders":        number,          // wallets with balance ≥ MIN_HOLD_TOKENS
  "engineWallet":   string | null,
  "dropIntervalMin": number
}
GET /api/drops

Last 40 drop records, newest first.

[{
  "id":             number,
  "at":             number,          // unix seconds
  "target_mint":    string,
  "target_symbol":  string,
  "target_name":    string,
  "reason":         string,          // why this coin was chosen
  "sol_spent":      number,
  "tokens_bought":  number,
  "recipients":     number,
  "swap_sig":       string,          // tx signature or "DRY_RUN"
  "status":         "done" | "skipped" | "failed",
  "note":           string
}]
09 — Database

Database schema

SQLite, WAL mode. File path: DB_PATH (default ./data/echo.db).

holders
wallet       TEXT PRIMARY KEY    -- base58 owner address
balance      REAL NOT NULL       -- UI units (raw / 10^decimals)
last_seen    INTEGER             -- unix seconds of last Helius observation
updated_at   INTEGER
drops
id             INTEGER PRIMARY KEY AUTOINCREMENT
at             INTEGER NOT NULL    -- unix seconds
target_mint    TEXT
target_symbol  TEXT
target_name    TEXT
reason         TEXT                -- "top boosted on dexscreener" etc.
sol_spent      REAL DEFAULT 0
tokens_bought  REAL DEFAULT 0
recipients     INTEGER DEFAULT 0
swap_sig       TEXT                -- tx signature or "DRY_RUN"
status         TEXT DEFAULT 'done' -- done | skipped | failed
note           TEXT
meta
key    TEXT PRIMARY KEY
value  TEXT

The meta table is used for persistent state across restarts (currently reserved for future use — state is reconstructed from drops and live RPC calls).

Resetting state. Run rm -rf data/ and restart. The directory and schema are re-created automatically. Do this when changing TOKEN_MINT — stale holder data for the old mint will cause incorrect recipient selection.

10 — Scripts

Scripts

npm run keygen

Generates a fresh Solana keypair using only node:crypto (no dependencies). Prints the public address and base58 private key. Use this to create the engine wallet — then launch your pump.fun coin from this wallet so it becomes the creator.

node scripts/keygen.js

→ Public address (use as the pump.fun creator wallet):
    73FL5Jwx...
→ POT_WALLET_SECRET (keep secret!):
    5Kd3NbFMvy...

node scripts/prelaunch.js <CA> [siteUrl]

Offline pre-launch helper. Takes your token's mint address (CA) and derives the pump.fun bonding curve address using the same PDA math as the Solana SDK. Outputs a ready-to-copy environment variable block.

node scripts/prelaunch.js CAnht1PfUb... https://yoursite.com

→ CA:            CAnht1PfUb...
→ Bonding curve: BZoQUSnD9p...   (must be in EXCLUDED_WALLETS)
→ ... full env block ...

The bonding curve exclusion is critical. The pump.fun bonding curve holds a large fraction of token supply. If it is not in EXCLUDED_WALLETS, the majority of every airdrop is sent to the curve address and irrecoverably lost.

11 — Operations

Running locally

Demo mode (zero keys)

MOCK=1 PORT=5000 node src/index.js

Simulated fees, synthetic holders, fake drops on a 40-second cycle. No chain calls. Safe for UI development and testing.

Dry-run mode (live reads, no transactions)

MOCK=0 DRY_RUN=1 \
  HELIUS_API_KEY=... \
  TOKEN_MINT=... \
  ENGINE_WALLET_SECRET=... \
  DROP_INTERVAL_MIN=2 \
  PORT=5000 node src/index.js

Polls real holders from Helius, picks a real trending coin, calculates a real buy — but sends no transactions. Drops are logged with swap_sig = DRY_RUN. Use a short DROP_INTERVAL_MIN (1–2) to quickly verify the full cycle works before going live.

Live mode

MOCK=0 DRY_RUN=0 PORT=5000 node src/index.js

Real swaps and real airdrops. Ensure ENGINE_WALLET_SECRET is set, the engine wallet is funded, and EXCLUDED_WALLETS includes the bonding curve + engine wallet + your personal wallets.

Production deployment. Use a Reserved VM (always-on). The 30-minute drop cycle requires a process that never sleeps. The run command is node src/index.js. Never hardcode PORT in the run command for production — let the hosting environment inject it.

12 — Launch checklist

Launch checklist

  • Run npm run keygen → save public address + store secret in ENGINE_WALLET_SECRET
  • Launch the pump.fun coin from the engine wallet address (this makes it the creator)
  • Run node scripts/prelaunch.js <CA> → copy the derived bonding curve address
  • Set TOKEN_MINT to the coin's CA
  • Set EXCLUDED_WALLETS = <bonding curve>,<engine wallet>,<your personal wallets>
  • Set BUY_URL = https://pump.fun/coin/<CA>
  • Set RAKE_WALLET to your personal wallet
  • Fund the engine wallet with ≥ 0.3 SOL (covers RESERVE_SOL + ATA rent + a few drops)
  • Start with DRY_RUN=1 and DROP_INTERVAL_MIN=2 — watch for [engine] 📡 DROP in logs
  • Verify the drop log shows real holders and a real Jupiter quote resolved
  • Flip DRY_RUN=0 and DROP_INTERVAL_MIN=30 — restart
  • Monitor the first live drop on Solscan before announcing publicly
13 — Security

Security notes

Engine wallet

The engine wallet holds real SOL and signs real transactions. Store ENGINE_WALLET_SECRET only in an environment secret manager (Replit Secrets, Doppler, etc.) — never in a file, never in a repo, never in a log.

API endpoints are public and unauthenticated

/api/state and /api/drops are intentionally public — transparency is a feature of the protocol. They are read-only. No write endpoints exist.

Bonding curve exclusion

The bonding curve address for any pump.fun token is deterministic and can be derived offline with scripts/prelaunch.js. Always derive it before launch and add it to EXCLUDED_WALLETS. Failure to do so means most of every airdrop is lost.

Recipient cap disclosure

The MAX_RECIPIENTS=150 cap and MIN_HOLD_TOKENS=100000 dust filter should be disclosed publicly. Holders below the threshold or outside the top 150 receive nothing from a given drop. This is by design — ATA rent makes unlimited recipients uneconomical.

DRY_RUN safety

The engine defaults to DRY_RUN=true. No transaction is ever signed or sent unless DRY_RUN is explicitly set to 0 or false. The config validator enforces that ENGINE_WALLET_SECRET is present before allowing DRY_RUN=0.