Skip to content

user-64bit/RugPulse

Repository files navigation

RugPulse

Real-time Solana new-token intelligence powered by Birdeye. Detect momentum. Expose concentration. Flag obvious risk.

Live: rugpulse.user64bit.wtf

A new Solana token launches every few seconds. Most are noise. The first sixty minutes decide whether one is worth watching, ignoring, or actively avoiding. RugPulse pulls fresh launches from Birdeye, enriches each one with security, holder, trader, and OHLCV data on the server, scores them deterministically, and surfaces a trader-readable verdict alongside the reasoning. No wallet connection. No swaps. No financial advice. Just a fast signal.

What it does

  • Detect momentum — early volume, trade count, and price action across 5m / 30m / 1h windows are weighted into an Alpha Score (0–100).
  • Expose concentration — top holder and top-10 percentages are surfaced in plain numbers and contribute to a Rug Risk Score (0–100).
  • Flag obvious risk — freeze authority, mint authority, mutable metadata, and owner-risk flags are read straight from Birdeye token_security and explained per token.

Every score on every token comes with per-factor explanations. No AI is used for scoring — the engine is deterministic, replayable, and auditable.

Demo flow

  1. Open rugpulse.user64bit.wtf.
  2. Radar shows the latest scanned launches with sparklines, scores, and verdict badges.
  3. Click any token → detail page with risk meter, score-breakdown bars, OHLCV chart, holder distribution, top traders, and security-flag cards.
  4. Leaderboard ranks launches by best alpha, highest momentum, lowest risk, most dangerous, or whale-heavy concentration.
  5. Alerts — open @RugPulseBot on Telegram, send /start subscribe, paste the 6-digit code, tune thresholds, get DM'd when a fresh launch hits all of them.
  6. Hit Generate X Post on any token to copy a 280-char draft with @birdeye_data #BirdeyeAPI attribution.

Powered by Birdeye

Six Birdeye Data Service endpoints are wired server-side. Every call carries the X-API-KEY and x-chain: solana headers; the API key is read with requireEnv() from a server-only module and never reaches the browser.

Endpoint Used for
GET /defi/v2/tokens/new_listing Discovery — fresh memecoin-platform launches
GET /defi/token_overview Market, volume, momentum, trade counts
GET /defi/token_security Authority, metadata, owner-risk flags
GET /defi/v3/token/holder Holder list + concentration math
GET /defi/v2/tokens/top_traders Buy/sell flow, sniper / dumping behavior
GET /defi/v3/ohlcv 1m and 5m candles for charts and sparklines

A single full scan = 1 + (enrich_limit × 5) Birdeye calls. With the production defaults (SCAN_LIMIT=10, ENRICH_LIMIT=5) every scan is 26 calls; at a 10-minute cadence that's ~3,744/day — well past the bounty's API-volume threshold.

Scoring

Both scores are 0–100 and clamped. Missing-data fields don't silently pass — they contribute a partial penalty so "no Birdeye signal" never reads as "clean."

Alpha Score — higher means more worth watching:

  • Liquidity quality: 20
  • 5m + 30m volume strength: 20
  • Trade activity: 15
  • Price momentum: 15
  • Holder count: 10
  • Security cleanliness: 10
  • Trader-flow quality: 10

Rug Risk Score — higher means more dangerous:

  • Top holder concentration: 20
  • Top 10 concentration: 20
  • Freeze authority active: 15
  • Mint authority active: 15
  • Mutable metadata / owner risk: 10
  • Low liquidity: 10
  • Suspicious trader imbalance: 10

Verdicts:

  • Watch — alpha ≥ 75 and risk ≤ 45
  • Caution — alpha ≥ 55 and risk ≤ 70
  • Avoid — risk ≥ 75
  • Neutral — everything else

Each score includes factor-level explanations ({factor, value, impact, severity, reason}) rendered on the token detail page.

Architecture

  • Frontend — Next.js App Router, TypeScript, Tailwind v4, shadcn-style components, Recharts.
  • API layerapp/api/* route handlers returning { ok, data, meta } or { ok, error }.
  • Birdeye clientlib/birdeye/* with server-only fetch, 10s timeout, bounded retries, dedicated 429 handling, defensive multi-alias normalizers.
  • Scoring — deterministic Alpha + Rug Risk + verdict + per-factor explanations in lib/scoring/*.
  • Persistence — Supabase Postgres tables for tokens, snapshots, scan runs, alert rules, alert matches, telegram subscribers.
  • Cache model — public reads hit Supabase first; cron and admin routes refresh Birdeye on demand.
  • Alerts — global rule evaluator runs at the end of every scan; matches dispatch to Telegram via per-subscriber chat binding.

API Routes

  • GET /api/health — env readiness + Supabase connectivity probe + latest scan summary.
  • GET|POST /api/cron/scan-new-tokens — bearer-auth scan trigger.
  • POST /api/admin/refresh-token — bearer-auth single-token refresh.
  • GET /api/tokens — radar list (batched snapshot reads).
  • GET /api/tokens/[address] — token detail.
  • GET /api/tokens/[address]/ohlcv — cached candles; admin-only refresh.
  • GET /api/leaderboard?type=best|momentum|lowest-risk|dangerous|whale-heavy — ranked lists.
  • GET /api/alerts — caller's rules (admin sees all).
  • POST /api/alerts — create rule, bound to subscriber.
  • PATCH|DELETE /api/alerts/[id] — update/delete owned rule.
  • GET /api/alerts/[id]/matches — last 100 match attempts.
  • POST /api/alerts/subscribe — redeem 6-digit Telegram claim code.
  • POST /api/telegram/webhook — Telegram-side entry point, validates X-Telegram-Bot-Api-Secret-Token.

Run locally

bun install
bun run dev

Checks:

bun run lint
bun run typecheck
bun run build

Health check:

curl http://localhost:3000/api/health

Trigger the first scan:

curl -X POST http://localhost:3000/api/cron/scan-new-tokens \
  -H "Authorization: Bearer $CRON_SECRET"

Then open /radar.

Environment

Copy .env.example and fill in:

BIRDEYE_API_KEY=
BIRDEYE_BASE_URL=https://public-api.birdeye.so
BIRDEYE_CHAIN=solana
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
CRON_SECRET=
ADMIN_SECRET=
TELEGRAM_BOT_TOKEN=
TELEGRAM_WEBHOOK_SECRET=
TELEGRAM_BOT_USERNAME=RugPulseBot
NEXT_PUBLIC_TELEGRAM_BOT_USERNAME=RugPulseBot
RUGPULSE_PUBLIC_URL=
RUGPULSE_SCAN_LIMIT=10
RUGPULSE_ENRICH_LIMIT=5
RUGPULSE_CACHE_TTL_SECONDS=60

BIRDEYE_API_KEY and SUPABASE_SERVICE_ROLE_KEY are server-only — never expose them as NEXT_PUBLIC_*. CRON_SECRET authorizes the scan endpoint; ADMIN_SECRET (or CRON_SECRET) authorizes manual refresh and admin alert ops. Subscriber-created alert rules are authorized by a per-chat UUID token issued through the Telegram bot.

Supabase

supabase db push

Schema:

  • tokens
  • token_security_snapshots
  • token_holder_snapshots
  • token_trader_snapshots
  • token_ohlcv_snapshots
  • scan_runs
  • alert_rules
  • alert_matches
  • telegram_subscribers

RLS is intentionally off — there is no per-user auth model. Public reads come through service-role-backed API routes; mutations are gated by Bearer tokens (admin secret, cron secret, or subscriber token).

Deployment (Oracle Always Free VM)

Production is an Oracle Cloud Always Free ARM Ampere VM running the Next.js app plus a local systemd timer that hits the scan endpoint every 10 minutes. All deployment scripts live under scripts/oracle-vm/:

File Purpose
rugpulse-app.service systemd unit running bun run start
rugpulse-app.env.example App env template — copy to /etc/default/rugpulse-app (mode 600)
rugpulse-scan.service + .timer Local scan timer fired every 10 minutes
rugpulse.env.example Cron env template — copy to /etc/default/rugpulse (mode 600)
rugpulse-scan.sh Curl worker with summary output
Caddyfile.snippet Reverse-proxy block for Caddy
set-telegram-webhook.sh Register the Telegram bot webhook
deploy.sh git fetch → reset → bun installbun run build → restart

One-time setup

# 1. Create the dedicated service user
sudo useradd -m -s /bin/bash rugpulse
sudo -u rugpulse bash -c 'curl -fsSL https://bun.sh/install | bash'

# 2. Clone the repo
sudo mkdir -p /opt/rugpulse-app
sudo chown rugpulse:rugpulse /opt/rugpulse-app
sudo -u rugpulse git clone <repo-url> /opt/rugpulse-app
cd /opt/rugpulse-app

# 3. App env (BIRDEYE_API_KEY + SUPABASE_SERVICE_ROLE_KEY — chmod 600)
sudo cp scripts/oracle-vm/rugpulse-app.env.example /etc/default/rugpulse-app
sudo vim /etc/default/rugpulse-app
sudo chmod 600 /etc/default/rugpulse-app

# 4. First build
sudo -u rugpulse bash -lc 'cd /opt/rugpulse-app && bun install --frozen-lockfile && bun run build'

# 5. systemd app unit
sudo cp scripts/oracle-vm/rugpulse-app.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now rugpulse-app.service
sudo systemctl status rugpulse-app.service --no-pager
curl -fsS http://127.0.0.1:3000/api/health | head -c 400

# 6. Caddy reverse-proxy — append the snippet to your Caddyfile
sudo $EDITOR /etc/caddy/Caddyfile
sudo systemctl reload caddy

# 7. Local scan timer (hits 127.0.0.1:3000 — no public HTTPS round-trip)
sudo mkdir -p /opt/rugpulse
sudo cp scripts/oracle-vm/rugpulse-scan.sh /opt/rugpulse/
sudo chmod +x /opt/rugpulse/rugpulse-scan.sh
sudo cp scripts/oracle-vm/rugpulse.env.example /etc/default/rugpulse
sudo vim /etc/default/rugpulse   # CRON_SECRET must match /etc/default/rugpulse-app
sudo chmod 600 /etc/default/rugpulse
sudo cp scripts/oracle-vm/rugpulse-scan.service /etc/systemd/system/
sudo cp scripts/oracle-vm/rugpulse-scan.timer   /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now rugpulse-scan.timer
systemctl list-timers rugpulse-scan.timer
journalctl -u rugpulse-scan.service -n 50 --no-pager

Subsequent deploys

sudo -u rugpulse /opt/rugpulse-app/scripts/oracle-vm/deploy.sh

git fetchgit reset --hard origin/mainbun install --frozen-lockfilebun run buildsystemctl restart rugpulse-app.service, then health-checks localhost:3000/api/health.

Operational notes

  • The scan timer fires every 10 minutes (OnUnitActiveSec=10min) plus once at boot. Persistent=true catches up missed fires after reboots.
  • The cron worker exits non-zero on HTTP failure, so systemctl status reflects real state and journalctl shows the response body.
  • RUGPULSE_ENRICH_LIMIT=5 is tuned for the Birdeye free tier at a 10-minute cadence. Raise it only on a paid plan; otherwise the scan will spend most of its time hitting 429.
  • RUGPULSE_SCAN_LIMIT=10 is the upstream new-listing fetch size. enrich_limit is the slice that gets fully enriched + scored. With both at default, a single scan completes well inside the 60-second app timeout.

If you prefer cron over systemd, the equivalent crontab line is:

*/10 * * * * RUGPULSE_URL=https://rugpulse.user64bit.wtf CRON_SECRET=... /opt/rugpulse/rugpulse-scan.sh >> /var/log/rugpulse-scan.log 2>&1

Telegram alerts

Alerts are bound to a Telegram chat, not to a website account. There's no email, no password, no wallet sign-in. The site is public, but every rule belongs to exactly one Telegram chat, and only that chat receives the notification.

How it works for a visitor

  1. Open /alerts and tap Open @RugPulseBot.
  2. The bot responds to /start subscribe with a one-time 6-digit code valid for 15 minutes.
  3. Paste the code into the alerts page and tap Verify code. The browser stores the returned subscriber_token in localStorage.
  4. Tune the thresholds (alpha, rug risk, liquidity, 5m volume, top holder %, top 10 %, freeze/mint authority) and tap Create alert rule.
  5. On the next scan that finds a launch matching every threshold, the bot DMs the chat with the score breakdown and a link back to /token/{address}.
  6. Each subscriber can keep up to five active rules. Pausing or deleting a rule is one tap on the rule card.

Matches are deduped per (rule, token) within a 30-minute window so the same launch never spams the same chat twice.

Operator setup (one-time)

After creating the bot in BotFather:

TELEGRAM_BOT_TOKEN=<from BotFather>
TELEGRAM_WEBHOOK_SECRET=<openssl rand -hex 24>
TELEGRAM_BOT_USERNAME=RugPulseBot
NEXT_PUBLIC_TELEGRAM_BOT_USERNAME=RugPulseBot
RUGPULSE_PUBLIC_URL=https://rugpulse.user64bit.wtf

Register the webhook (Telegram requires HTTPS, so this must point at the Caddy-fronted origin):

sudo -u rugpulse \
  TELEGRAM_BOT_TOKEN=... \
  TELEGRAM_WEBHOOK_SECRET=... \
  RUGPULSE_PUBLIC_URL=https://rugpulse.user64bit.wtf \
  /opt/rugpulse-app/scripts/oracle-vm/set-telegram-webhook.sh

Verify with curl -s https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo | jqpending_update_count should be 0 and last_error_message should be empty.

If TELEGRAM_BOT_TOKEN is unset on a deployment the system stays functional: subscribe attempts return 503, and rule matches land in alert_matches with delivery_status = "disabled".

Empty / error states

The frontend reads exclusively from the Supabase cache populated by the scan timer. There is no mock fallback in production paths. When the DB is empty or an API request fails:

  • Radar shows "Awaiting first scan" with instructions to trigger the protected scan endpoint.
  • Leaderboard shows "No leaderboard data yet".
  • Alerts shows "No persisted rules".
  • Token detail shows "Token not found" with the API error message if any.

A local-only file shim (.rugpulse-cache/store.json) activates only when Supabase returns a "table not found" error — useful for local dev before migrations are applied, dead code in production.

Known limitations

  • No site-level auth or per-user accounts — ownership is identity-by-Telegram-chat.
  • Discord and email delivery are not wired; only Telegram is supported today.
  • The match snapshot jsonb is the source of truth — matches_24h_count on the rule row is intentionally not maintained as a rolling counter.
  • Public token-detail and chart routes are cache-first; live refresh requires admin authorization.
  • Birdeye response shapes are parsed defensively, but new endpoint fields may need additional normalizer aliases.
  • Scoring thresholds are hand-tuned heuristics, not a backtested model.
  • No wallet connection, swaps, or trading actions are implemented.

Bounty notes

RugPulse demonstrates a production-oriented Birdeye backend layer: six endpoints, server-side API-key handling, multi-endpoint enrichment, defensive normalization, deterministic explainable scoring, Supabase caching with batched reads, protected cron and admin refresh, end-to-end Telegram subscriber alerts with rule evaluation at the end of every scan, and a self-hosted Oracle VM deployment with systemd-driven 10-minute scan cadence. The frontend is server-first where it matters (landing, scoring) and client-fetched where freshness matters (radar, detail, leaderboard, alerts). No mock data leaks into production paths.

Signals are informational only. Not financial advice.

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors