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.
- 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_securityand 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.
- Open rugpulse.user64bit.wtf.
- Radar shows the latest scanned launches with sparklines, scores, and verdict badges.
- Click any token → detail page with risk meter, score-breakdown bars, OHLCV chart, holder distribution, top traders, and security-flag cards.
- Leaderboard ranks launches by best alpha, highest momentum, lowest risk, most dangerous, or whale-heavy concentration.
- Alerts — open
@RugPulseBoton Telegram, send/start subscribe, paste the 6-digit code, tune thresholds, get DM'd when a fresh launch hits all of them. - Hit Generate X Post on any token to copy a 280-char draft with
@birdeye_data #BirdeyeAPIattribution.
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.
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 ≤ 45Caution— alpha ≥ 55 and risk ≤ 70Avoid— risk ≥ 75Neutral— everything else
Each score includes factor-level explanations ({factor, value, impact, severity, reason}) rendered on the token detail page.
- Frontend — Next.js App Router, TypeScript, Tailwind v4, shadcn-style components, Recharts.
- API layer —
app/api/*route handlers returning{ ok, data, meta }or{ ok, error }. - Birdeye client —
lib/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.
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, validatesX-Telegram-Bot-Api-Secret-Token.
bun install
bun run devChecks:
bun run lint
bun run typecheck
bun run buildHealth check:
curl http://localhost:3000/api/healthTrigger the first scan:
curl -X POST http://localhost:3000/api/cron/scan-new-tokens \
-H "Authorization: Bearer $CRON_SECRET"Then open /radar.
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=60BIRDEYE_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 db pushSchema:
tokenstoken_security_snapshotstoken_holder_snapshotstoken_trader_snapshotstoken_ohlcv_snapshotsscan_runsalert_rulesalert_matchestelegram_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).
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 install → bun run build → restart |
# 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-pagersudo -u rugpulse /opt/rugpulse-app/scripts/oracle-vm/deploy.shgit fetch → git reset --hard origin/main → bun install --frozen-lockfile → bun run build → systemctl restart rugpulse-app.service, then health-checks localhost:3000/api/health.
- The scan timer fires every 10 minutes (
OnUnitActiveSec=10min) plus once at boot.Persistent=truecatches up missed fires after reboots. - The cron worker exits non-zero on HTTP failure, so
systemctl statusreflects real state andjournalctlshows the response body. RUGPULSE_ENRICH_LIMIT=5is 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 hitting429.RUGPULSE_SCAN_LIMIT=10is the upstream new-listing fetch size.enrich_limitis 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
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.
- Open
/alertsand tap Open @RugPulseBot. - The bot responds to
/start subscribewith a one-time 6-digit code valid for 15 minutes. - Paste the code into the alerts page and tap Verify code. The browser stores the returned
subscriber_tokeninlocalStorage. - Tune the thresholds (alpha, rug risk, liquidity, 5m volume, top holder %, top 10 %, freeze/mint authority) and tap Create alert rule.
- 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}. - 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.
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.wtfRegister 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.shVerify with curl -s https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo | jq — pending_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".
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.
- 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
snapshotjsonb is the source of truth —matches_24h_counton 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.
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.