Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ChainAlert — crypto price & whale alert bot

tests

A Telegram/Discord alert bot built on the poll-once, serve-many pattern: a scheduled poller makes one batched call per cycle into a cache, and every user is served from that cache. User count never touches an API, so free tiers comfortably serve thousands of subscribers.

Two data sources, because no single one covers both needs:

Status: complete, running Laravel 12 app, wired to the real APIs and verified live — CoinGecko returns real prices, the explorer returns real chain heights, the whale scanner found 5 transfers ≥ 50 BTC (top 535 BTC) in a real block, wallet-activity (BTC/BCH/ETH/SOL) isolates new transactions on live addresses, token-activity (ERC-20/SPL) detects real ERC-20 movement on a live ETH address, and internal-transaction activity isolates new contract-driven txs on a live ETH address. 39 feature tests green (vendor/bin/phpunit).

Architecture

CoinGecko  ──(batched /simple/price, 2 min)──▶ poll-prices ─┐
                                                            ├─▶ PriceCache (Redis + DB mirror)
                                          RuleMatcher ◀─────┘
                                              │ price rules past cooldown
blockchain.com ──(new blocks, 15 min)───▶ poll-blocks  ──▶ WhaleScanner
blockchain.com ──(watched addrs, 15 min)─▶ poll-wallets ──▶ WalletWatcher
blockchain.com ──(token holders, 30 min)─▶ poll-tokens  ──▶ TokenWatcher
blockchain.com ──(ETH addrs, 30 min)─────▶ poll-internal ─▶ WalletWatcher (id-diff)
                                              │ whale_tx / wallet_activity / token_activity / internal_activity rules past cooldown
                                              ▼
                          AlertEvent + Delivery ──▶ queue ──▶ DeliverAlertJob
                                                                  │
                                                TelegramChannel / DiscordChannel

Bot commands (/watch, /whale, /wallet, /token, /internal, /list, /stop) ──▶ TelegramWebhookController ──▶ AlertRule store
        (this path NEVER calls any external API)

Alert types

Command Type Source How it fires
/watch BTC > 70000 price_above CoinGecko price ≥ threshold
/watch BTC < 60000 price_below CoinGecko price ≤ threshold
/watch ETH %5 percent_move CoinGecko |Δ| ≥ N% vs last reading
/whale BTC 500 whale_tx blockchain.com a single tx output ≥ N coins in a new block
/wallet BTC bc1q… wallet_activity blockchain.com any new transaction on a watched address
/token ETH 0x… token_activity blockchain.com ERC-20/SPL token holdings on a watched address moved
/internal ETH 0x… internal_activity blockchain.com a new contract-driven (internal) transaction touched a watched address

Chain coverage: prices — BTC/ETH/SOL/BCH · whale — BTC/BCH only (see budget) · wallet — BTC/BCH/ETH/SOL · token — ETH/SOL only (ERC-20/SPL) · internal — ETH only (EVM concept). To track a big ETH/SOL wallet, /wallet its address — that's the feasible substitute for network-wide ETH/SOL whale scanning; add /token to also catch its ERC-20/SPL movements, and /internal (ETH) to catch contract-driven value moves the external feed never shows.

/wallet vs /token: they read different data and are complementary. /wallet diffs the address's transaction feed (external txs, newest-first ids). /token diffs the address's token-balance snapshot — the explorer exposes no token-transfer feed, only balances, so a move is inferred from a changed balance (SOL) or an incremented per-token transferCount (ETH). See "The two APIs".

What's included

Layer File
Config + env config/alertbot.php, env.alertbot.example
Schema database/migrations/2026_07_19_2000* (subscribers, alert_rules, alert_events + context, deliveries, price_snapshots)
Models app/Models/{Subscriber,AlertRule,AlertEvent,Delivery,PriceSnapshot}.php
Price seam app/Services/PriceApiClient.php (CoinGecko)
On-chain seam app/Services/ExplorerApiClient.php (blockchain.com)
Whale logic app/Services/Whale/WhaleScanner.php
Wallet logic app/Services/Wallet/WalletWatcher.php
Token logic app/Services/Token/TokenWatcher.php
Internal-tx logic reuses app/Services/Wallet/WalletWatcher.php (same id-diff)
Cache app/Services/PriceCache.php
Price matching app/Services/RuleMatcher.php
Delivery app/Services/Channels/{AlertChannel,TelegramChannel,DiscordChannel,ChannelManager}.php
Pollers app/Console/Commands/{PollPricesCommand,PollBlocksCommand,PollWalletsCommand,PollTokensCommand,PollInternalCommand}.php
Queue worker app/Jobs/DeliverAlertJob.php
Bot commands app/Http/Controllers/TelegramWebhookController.php
Wiring routes/api.php, routes/console.php

The two APIs

CoinGeckoGET {base}/simple/price?ids=bitcoin,ethereum,…&vs_currencies=usd&include_24hr_change=true{"bitcoin":{"usd":64457,"usd_24h_change":-0.17}, …}. No key required at low volume; a demo key (x-cg-demo-api-key) lifts the free rate/monthly caps. Symbol → id map lives in config/alertbot.php (prices.ids).

blockchain.com Explorer Gateway — base https://api.blockchain.info/explorer-gateway-kt, all endpoints POST, API key optional in the X-Explorer-Auth-Key header (no key = lower default rate limits). Used:

  • Whale (BTC/BCH): POST /{c}/blocks {}{currentHeight, blocks}, then POST /{c}/block {height}{txs:[{txId, inputs:[{coinbase}], outputs:[{value(sats),address}]}]}
  • Wallet BTC/BCH: POST /{c}/address/transactions {address}{transactions:[{txId, …}]} (newest-first)
  • Wallet ETH: POST /eth/address {address, network}{transactions:[{hash, from, to, value(wei)}]}
  • Wallet SOL: POST /sol/address {address, network}{transactions:[{txId, …}]}
  • Token ETH: POST /eth/address/tokens {address, network}{tokens:[{contractAddress, balance, totalSent, totalReceived, transferCount, symbol}]}
  • Token SOL: POST /sol/address/tokens {address, network}{tokens:[{mint, tokenWallet, balance, symbol}]}
  • Internal ETH: POST /eth/address/internal-transactions {address, network}{internalTransactions:[{transactionHash, blockNumber, timestamp, from, to, value, type}]} (newest-first)

ETH/SOL calls require a network (default mainnet, see explorer.networks). ExplorerApiClient::addressTransactionIds() hides these per-chain field/endpoint quirks and returns a normalised, newest-first list of id strings; WalletWatcher diffs them against the stored watermark.

Token endpoints are balance SNAPSHOTS, not transfer feeds. The gateway exposes no ERC-20/SPL transfer feed (/eth/address/token-transfers, /transfers, etc. all 404), so there is no per-transfer id to diff. ExplorerApiClient::addressTokenBalances() normalises each holding to id → {counter, symbol} (id = ERC-20 contract / SPL mint), and TokenWatcher diffs two snapshots instead of an id list. The counter differs by chain: ETH exposes a monotonic per-token transferCount, so the diff yields the exact number of new transfers; SOL exposes only balance, so a change just signals that a position moved (magnitude = number of positions that changed/appeared/disappeared). This is why token-activity is a separate poller (PollTokensCommand, snapshot-diff) from wallet-activity (PollWalletsCommand, id-diff) rather than a branch inside it. One caveat of balance-diffing on SOL: airdropped dust changes the snapshot and can alert.

Internal transactions ARE a real id feed (unlike tokens). /eth/address/internal-transactions returns newest-first entries keyed by transactionHash, so addressInternalTransactionIds() reuses the exact same id-diff as external wallet activity — PollInternalCommand shares WalletWatcher untouched, with its own internal:last:* watermark. One quirk: a single transaction can spawn several internal txs that all carry the same parent transactionHash, so the ids are de-duplicated (order-preserving) — one transaction = one unit of activity. It's kept as an opt-in /internal command (ETH only — internal txs are an EVM concept) rather than folded into /wallet, so the extra explorer call is spent only for addresses a user explicitly watches, and existing /wallet behavior is unchanged.

Whale metric: a transaction's largest single output ≥ threshold (sats ÷ 1e8), excluding coinbase — see WhaleScanner. ETH/SOL wallet-activity (/wallet) covers external transactions only; ERC-20/SPL token movement is covered by /token, and ETH internal (contract-driven) transactions by /internal — three complementary feeds over the same watched address.

Quota budget

Cadences ship conservative by default so a keyless blockchain.com free tier (1,000 calls/day) stays under budget, and every cadence is tunable via ALERTBOT_CRON_* env vars (see config/alertbot.php) — no code change, just redeploy. With an EXPLORER_API_KEY (raises the ceiling) you can shorten blocks/wallets for fresher alerts. Defaults: prices */2, blocks/wallets */15, tokens/internal */30.

Prices (CoinGecko, its own limits): one batched call covers every coin and every user. Every 2 min = 720/day. If you hit CoinGecko's monthly cap, add a demo key or lengthen the cadence.

Whale (blockchain.com, 1,000/day free tier):

Chains Cadence ~Calls/day Verdict
BTC + BCH every 15 min (default) ~480 ✅ leaves headroom for address polls
BTC + BCH every 5 min ~600–900 ✅ under 1,000 alone (needs a key to add the rest)
+ ETH/SOL any ❌ fast blocks — too many block bodies/day

The fixed-cost floor is the per-block body fetches (~288/day for BTC+BCH regardless of cadence); a longer cadence mainly trims the latest-blocks calls. Whale scanning is poll-once-serve-many and viable only on slow UTXO chains (BTC/BCH); ETH/SOL blocks are far too fast to fetch per-block on the free tier. A per-cycle block cap (ALERTBOT_WHALE_MAX_BLOCKS) stops a post-downtime catch-up from blowing the day.

Wallet activity (blockchain.com, shares the same daily quota): one call per distinct watched address per cycle (shared addresses deduped), so cost scales with unique addresses, not users. At the default 15 min that's 96 × (unique addresses) calls/day, competing with whale for the 1,000/day budget — so use an API key (X-Explorer-Auth-Key) as usage grows, and lengthen the cadence or cap watched addresses if you're near the ceiling.

Token activity (blockchain.com, shares the same daily quota): same poll-once shape — one /address/tokens call per distinct watched ETH/SOL address per cycle. At the default 30 min that's 48 × (unique token addresses) calls/day, on top of whale + wallet — the lightest of the on-chain polls.

Internal transactions (blockchain.com, shares the same daily quota): one /eth/address/internal-transactions call per distinct watched ETH address per cycle. At the default 30 min that's 48 × (unique internal addresses) calls/day. It's opt-in (/internal), so it costs nothing until a user watches an address — but with whale + wallet + token + internal all active and real usage, budget for an API key.

Run it

Already scaffolded, migrated (SQLite), tested, and live-verified. To run locally:

composer install                  # if vendor/ is missing
php artisan migrate               # already done here; safe to re-run

# For production, back cache + queue with Redis (edit .env):
#   CACHE_STORE=redis  QUEUE_CONNECTION=redis
composer require predis/predis    # or the phpredis extension

# Fill in the keys (see env.alertbot.example), then run four processes:
php artisan schedule:work         # poll-prices (2m) + poll-blocks/wallets (15m) + poll-tokens/internal (30m)
php artisan queue:work            # delivers alerts
php artisan serve                 # receives the Telegram webhook

Try the pollers directly (they hit the live APIs):

php artisan alertbot:poll-prices  # → "Polled 4 assets, fired N alerts."
php artisan alertbot:poll-blocks  # → initializes chain heights, then scans new blocks
php artisan alertbot:poll-wallets # → seeds watched addresses, then detects new txs
php artisan alertbot:poll-tokens  # → seeds token holdings, then detects token movement
php artisan alertbot:poll-internal # → seeds internal txs, then detects new contract-driven txs

Run the tests with vendor/bin/phpunit (39 feature tests: price pipeline, cooldown, API-outage fallback, whale detection + first-run seeding, wallet activity across BTC/ETH/SOL incl. shared-address dedupe, token activity (ETH transferCount deltas + SOL balance diffs) incl. shared-address dedupe, internal-tx activity incl. parent-hash de-dupe + shared-address dedupe, webhook commands incl. /whale, /wallet, /token & /internal, and the plan cap).

Windows/XAMPP note: php artisan test may fail with a "Cannot declare class ComposerAutoloaderInit…" fatal — a known collision-wrapper quirk. Run php vendor/phpunit/phpunit/phpunit (or vendor/bin/phpunit) instead.

Deploy to production (Railway)

The whole bot ships as one Railway service (Docker) running web + queue + scheduler under supervisor, backed by Railway PostgreSQL (which also holds the cache + queue — no Redis needed to start). All artifacts are in the repo: Dockerfile, docker/entrypoint.sh, docker/supervisord.conf, railway.toml, .dockerignore, .env.railway.example. Full step-by-step (BotFather → Railway → setWebhook → verify): see DEPLOY.md.

Point Telegram at the webhook

curl "https://api.telegram.org/bot<TOKEN>/setWebhook" \
  -d "url=https://your-domain/api/webhooks/telegram" \
  -d "secret_token=<TELEGRAM_WEBHOOK_SECRET>"

Keys

Put your real keys in .env (never in code):

  • EXPLORER_API_KEY — your blockchain.com Explorer key (optional; raises limits).
  • COINGECKO_API_KEY — optional demo key.

Deliberately out of scope (clear next steps)

  • ETH/SOL whale (network-wide) — infeasible on a modest quota: ETH mines ~7,200 blocks/day and SOL ~200k+ slots/day, so you can't scan every block. Watch specific large addresses via /wallet instead — the practical whale-tracking substitute.
  • ETH/SOL token transfers — ✅ done via /token (PollTokensCommand + TokenWatcher), by diffing the /address/tokens balance snapshot (the gateway has no token-transfer feed). ETH gets exact transfer counts (transferCount delta); SOL gets movement signals from balance changes. See "The two APIs".
  • ETH internal transactions — ✅ done via /internal (PollInternalCommand + addressInternalTransactionIds(), reusing WalletWatcher's id-diff). Kept opt-in and separate from /wallet on purpose (budget + no behavior change). See "The two APIs". Could be folded into /wallet ETH later (both feeds carry blockNumber, so a merge-by-block would be safe) if one-command completeness is preferred over opt-in cost.
  • ERC-20/SPL token amounts/token alerts that holdings moved, not by how much. ETH totalSent/totalReceived and SOL balance deltas are the hooks to add a threshold (e.g. "alert only on token moves ≥ X").
  • No buy/sell signals or advice — data and alerts only, by design.
  • Reselling raw API data usually violates vendor terms; the value you sell is the alerts/UI on top.
  • Payment/subscription flow (plan column + Subscriber::ruleLimit() are the hooks; wire your billing provider to flip plan).
  • Discord slash-command intake (only Telegram intake is built; Discord is wired as an outbound channel).

About

Laravel crypto price & on-chain alert bot (Telegram/Discord) — poll-once-serve-many; price/whale/wallet/token alerts

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages