Chain to inbox
Four stages, each independently testable. Splitting ingestion from delivery is what lets the free tier have a real delivery delay, lets a Telegram outage stall delivery without stalling ingestion, and keeps a slow platform from pushing the block cursor behind the chain.
Confirmed blocks
Both launchpads plus tracked Uniswap v3 pools, read through a persisted per-source cursor.
Normalized events
launch, curve_trade, graduation, whale_trade, each with a USD value or an honest null.
Rule engine
Cheap filters first, on-chain lookups only when a rule actually asks for one.
Outbox and delivery
Deduped, tier-delayed, retried with backoff, dead-lettered when a target is gone for good.
Two launchpads, two lifecycles
This is the detail an alert product has to get right. NOXA and The Odyssey are not the same machine, and pretending they are produces alerts that are false rather than merely noisy.
NOXA
- One transaction deploys the ERC-20, creates a Uniswap v3 pool, seeds single-sided liquidity and locks the LP NFT.
- No bonding curve exists, so there is nothing to fill.
- Trades as ordinary Uniswap v3 swaps from block one.
- A NOXA graduation is not a thing. Any product that offers one is describing an event that never fires.
The Odyssey
- A launch opens a curve. No pool exists yet, so the launch has no dollar value attached.
- Every buy and sell on the curve emits
Traded, priced through its native-ETH leg. - When the curve fills,
PoolCompletedandPoolMigratedmove liquidity to a locked Uniswap v3 pool. - After graduating, its tokens produce whale trades like any other pool.
safeParseRule({ id: 'nope', kinds: ['graduation'], launchpads: ['noxa'] })
// success: false
// "this combination can never fire: NOXA is an instant launcher with no
// bonding curve, so it emits no curve_trade and no graduation"
The event taxonomy
Every event carries a stable id derived from its transaction hash and log index, which is what makes replaying a block range after a crash safe.
| Kind | Decoded from | Launchpads | usdValue is |
|---|---|---|---|
launch |
NOXA TokenLaunched, Odyssey TokenCreated |
both | the deployer's initial buy (NOXA), or null on Odyssey, where the curve holds the liquidity |
curve_trade |
Odyssey Traded |
Odyssey only | the native-ETH leg, priced through live WETH/USDG liquidity |
graduation |
Odyssey PoolMigrated |
Odyssey only | the quote side used to seed the migrated pool |
whale_trade |
Uniswap v3 Swap on a tracked memecoin pool |
both | the swap's quote leg |
Every source, decoded against mainnet
npm run verify:chain runs the whole read path against the public RPC with no
credentials, no database and no writes. Replaying the launchpads' first 1.44 million blocks:
chain 4663, head block 15010850
ETH/USD from live Uniswap v3 liquidity: $1905.26
scanning blocks 61688 to 1500000 (1438313 blocks)
NOXA launches 2621 events
launch ? 0x6399E2Bd8af62C0ac13f55613C3469b67332a6Fd $266.74 block 61869
launch HUSK 0x57EB9C9153cfE0277F91Ff8B8604C2D3006a9196 $9.53 block 61987
Odyssey launches 4 events
launch ROBIN 0xfB4729659eeF22Bfc1c2B680F6F873f8147aaaab unpriced block 983265
Odyssey curve trades 110 events
curve_trade ROBIN 0xfB4729659eeF22Bfc1c2B680F6F873f8147aaaab $18.67 block 983265
Odyssey graduations 1 events
graduation ROBIN 0xfB4729659eeF22Bfc1c2B680F6F873f8147aaaab $7618.47 block 1048638
Whale trades 4045 events
whale_trade CHEEMS 0xdaA213A0Bd8B048D6022e2c46df877E8A204072b $190.46 block 1439024
pools discovered and registered for whale watching: 2622
Follow ROBIN down those four rows and the taxonomy proves itself: a curve opens (unpriced,
because a curve holds no pool), trades on the curve, then fills and migrates with $7,618.47 of
liquidity seeded. NOXA tokens never appear in the middle two rows, because they cannot. The
first NOXA token has no symbol() and renders as ? rather than
dropping the alert.
null, and a
minUsd rule does not match it. Unknown is never quietly treated as zero.
Surviving a restart mid-stream
An alert bot that double-sends is spam, and one that skips a range misses the launch its subscribers paid for. Three guarantees, each held by a specific mechanism and each covered by a test that simulates the failure.
Never double-send
Every delivery has a deterministic key, eventId|subscriptionId|ruleId, and the event id comes from the transaction hash and log index. Re-processing a range regenerates identical keys, so the second pass inserts nothing.
Never silently skip
The block cursor advances only after every event in a chunk is enqueued and committed. A crash mid-chunk, or a failed RPC call, leaves it pointing at the start of that range, so the range is re-read.
Never lose a queued alert
Queued rows live in an outbox until delivered or dead-lettered. On startup, rows abandoned in sending by a crash return to pending and are retried.
Delivery that respects each platform's actual contract
| Telegram | Discord | |
|---|---|---|
| Rate limit signal | parameters.retry_after, whole seconds, on a 429 |
Per-route bucket headers on every response, plus retry_after as a float |
| Correct behaviour | Wait exactly that long, capped so one throttled chat cannot stall the loop | Do not send once a bucket is exhausted; apply a global 429 to every route |
| Escaping | 18 reserved characters in MarkdownV2 text, 2 in a link URL, 2 in a code span, 3 in HTML | Markdown rendered inside embed titles, descriptions and field values |
| Permanent failures | Blocked bot, deleted chat: dead-lettered, never retried | Unknown webhook, missing permission: dead-lettered |
| Command transport | Long polling: no public URL, TLS or firewall rule needed | Slash commands over an Ed25519-verified interactions endpoint |
WHO_LET_THE italicises half a
MarkdownV2 message and a token called <b>RUG injects markup under HTML parse
mode; either way Telegram answers 400 can't parse entities and the alert is lost.
Each context has its own escaper and its own tests, and truncation is surrogate-pair aware so a
long emoji-bearing name is cut cleanly.
Tiers are enforced, not advertised
Every limit maps to a real cost, and the check lives in exactly one place. The bot asks it before accepting a rule; the dispatcher asks it before queuing a delivery. Nothing else reads the policy table, so the two cannot disagree about who is premium.
| Limit | Free | Premium |
|---|---|---|
| Subscriptions | 1 | 10 |
| Rules per subscription | 3 | 50 |
| Filters per rule | 3 | 24 |
| Watchlist size | 5 | 500 |
| Alerts per hour | 30 | 2,000 |
| Delivery delay | 60s | none |
| Liquidity and reputation filters | no | yes |
| Event kinds | launch, graduation, whale_trade | all four, including the curve firehose |
Transfer logs from a signature-linked wallet to the operator's receiving address and
accrues subscription time from those real payments: no facilitator, no card processor, no third
party. hood-alerts deliberately does not wire an x402 rail, because x402 prices a single
HTTP request and a subscription is not a request.
What it does not do
Watch every pool on the chain
A topic-only Uniswap v3 Swap query overflows the public RPC's 10,000-log result cap in under 2,000 blocks. Whale trades cover a tracked pool set built from launchpad activity, with subscriber watchlists pinned on top of the cap.
Run on testnet
Neither launchpad is deployed on chain 46630. Building sources against it throws rather than reporting an empty chain as "no launches".
Call a rug a rug
The drained-pool figure is a defined heuristic: a prior launch whose quote reserve is now below a threshold. A token that never traded looks the same. It is evidence, not a verdict.
Measure tradeable depth
Liquidity filters read total pool reserves. For concentrated positions the reserve inside the active tick can be far smaller.