hood-alerts 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.env.example +90 -0
  2. package/Dockerfile +65 -0
  3. package/LICENSE +15 -14
  4. package/README.md +253 -309
  5. package/dist/scripts/capture-fixtures.d.ts +2 -0
  6. package/dist/scripts/capture-fixtures.js +143 -0
  7. package/dist/scripts/capture-fixtures.js.map +1 -0
  8. package/dist/scripts/grant-premium.d.ts +2 -0
  9. package/dist/scripts/grant-premium.js +70 -0
  10. package/dist/scripts/grant-premium.js.map +1 -0
  11. package/dist/scripts/live-probe.d.ts +2 -0
  12. package/dist/scripts/live-probe.js +51 -0
  13. package/dist/scripts/live-probe.js.map +1 -0
  14. package/dist/src/commands/commands.d.ts +42 -0
  15. package/dist/src/commands/commands.js +246 -0
  16. package/dist/src/commands/commands.js.map +1 -0
  17. package/dist/src/config.d.ts +69 -0
  18. package/dist/src/config.js +72 -0
  19. package/dist/src/config.js.map +1 -0
  20. package/dist/src/db/deliveries.d.ts +17 -0
  21. package/dist/src/db/deliveries.js +41 -0
  22. package/dist/src/db/deliveries.js.map +1 -0
  23. package/dist/src/db/entitlements.d.ts +41 -0
  24. package/dist/src/db/entitlements.js +73 -0
  25. package/dist/src/db/entitlements.js.map +1 -0
  26. package/dist/src/db/index.d.ts +4 -0
  27. package/dist/src/db/index.js +63 -0
  28. package/dist/src/db/index.js.map +1 -0
  29. package/dist/src/db/subscribers.d.ts +44 -0
  30. package/dist/src/db/subscribers.js +124 -0
  31. package/dist/src/db/subscribers.js.map +1 -0
  32. package/dist/src/engine/detectors/holders.d.ts +16 -0
  33. package/dist/src/engine/detectors/holders.js +33 -0
  34. package/dist/src/engine/detectors/holders.js.map +1 -0
  35. package/dist/src/engine/detectors/liquidity.d.ts +24 -0
  36. package/dist/src/engine/detectors/liquidity.js +62 -0
  37. package/dist/src/engine/detectors/liquidity.js.map +1 -0
  38. package/dist/src/engine/detectors/live.d.ts +52 -0
  39. package/dist/src/engine/detectors/live.js +420 -0
  40. package/dist/src/engine/detectors/live.js.map +1 -0
  41. package/dist/src/engine/detectors/premium-ladder.d.ts +14 -0
  42. package/dist/src/engine/detectors/premium-ladder.js +61 -0
  43. package/dist/src/engine/detectors/premium-ladder.js.map +1 -0
  44. package/dist/src/engine/detectors/prices.d.ts +15 -0
  45. package/dist/src/engine/detectors/prices.js +45 -0
  46. package/dist/src/engine/detectors/prices.js.map +1 -0
  47. package/dist/src/engine/detectors/whale-classify.d.ts +18 -0
  48. package/dist/src/engine/detectors/whale-classify.js +69 -0
  49. package/dist/src/engine/detectors/whale-classify.js.map +1 -0
  50. package/dist/src/engine/digest.d.ts +31 -0
  51. package/dist/src/engine/digest.js +48 -0
  52. package/dist/src/engine/digest.js.map +1 -0
  53. package/dist/src/engine/engine.d.ts +40 -0
  54. package/dist/src/engine/engine.js +175 -0
  55. package/dist/src/engine/engine.js.map +1 -0
  56. package/dist/src/engine/eth-price.d.ts +18 -0
  57. package/dist/src/engine/eth-price.js +85 -0
  58. package/dist/src/engine/eth-price.js.map +1 -0
  59. package/dist/src/engine/events.d.ts +94 -0
  60. package/dist/src/engine/events.js +37 -0
  61. package/dist/src/engine/events.js.map +1 -0
  62. package/dist/src/engine/gate.d.ts +35 -0
  63. package/dist/src/engine/gate.js +35 -0
  64. package/dist/src/engine/gate.js.map +1 -0
  65. package/dist/src/engine/token-meta.d.ts +17 -0
  66. package/dist/src/engine/token-meta.js +44 -0
  67. package/dist/src/engine/token-meta.js.map +1 -0
  68. package/dist/src/engine/topics.d.ts +60 -0
  69. package/dist/src/engine/topics.js +120 -0
  70. package/dist/src/engine/topics.js.map +1 -0
  71. package/dist/src/format/cards.d.ts +46 -0
  72. package/dist/src/format/cards.js +193 -0
  73. package/dist/src/format/cards.js.map +1 -0
  74. package/dist/{service/main.d.cts → src/index.d.ts} +1 -0
  75. package/dist/src/index.js +133 -0
  76. package/dist/src/index.js.map +1 -0
  77. package/dist/src/logger.d.ts +5 -0
  78. package/dist/src/logger.js +16 -0
  79. package/dist/src/logger.js.map +1 -0
  80. package/dist/src/premium/paywall.d.ts +40 -0
  81. package/dist/src/premium/paywall.js +105 -0
  82. package/dist/src/premium/paywall.js.map +1 -0
  83. package/dist/src/server.d.ts +14 -0
  84. package/dist/src/server.js +57 -0
  85. package/dist/src/server.js.map +1 -0
  86. package/dist/src/transports/console.d.ts +14 -0
  87. package/dist/src/transports/console.js +24 -0
  88. package/dist/src/transports/console.js.map +1 -0
  89. package/dist/src/transports/discord.d.ts +28 -0
  90. package/dist/src/transports/discord.js +145 -0
  91. package/dist/src/transports/discord.js.map +1 -0
  92. package/dist/src/transports/telegram.d.ts +22 -0
  93. package/dist/src/transports/telegram.js +101 -0
  94. package/dist/src/transports/telegram.js.map +1 -0
  95. package/dist/src/transports/types.d.ts +14 -0
  96. package/dist/src/transports/types.js +2 -0
  97. package/dist/src/transports/types.js.map +1 -0
  98. package/dist/src/transports/x.d.ts +58 -0
  99. package/dist/src/transports/x.js +155 -0
  100. package/dist/src/transports/x.js.map +1 -0
  101. package/package.json +80 -112
  102. package/dist/bot/index.cjs +0 -1008
  103. package/dist/bot/index.cjs.map +0 -1
  104. package/dist/bot/index.d.cts +0 -241
  105. package/dist/bot/index.d.ts +0 -241
  106. package/dist/bot/index.js +0 -30
  107. package/dist/bot/index.js.map +0 -1
  108. package/dist/chunk-32GS6XVE.js +0 -732
  109. package/dist/chunk-32GS6XVE.js.map +0 -1
  110. package/dist/chunk-3LHCQA4Z.js +0 -76
  111. package/dist/chunk-3LHCQA4Z.js.map +0 -1
  112. package/dist/chunk-3NHLSKZE.js +0 -1
  113. package/dist/chunk-3NHLSKZE.js.map +0 -1
  114. package/dist/chunk-6JQK5G3Z.js +0 -72
  115. package/dist/chunk-6JQK5G3Z.js.map +0 -1
  116. package/dist/chunk-CBNZ6AZT.js +0 -598
  117. package/dist/chunk-CBNZ6AZT.js.map +0 -1
  118. package/dist/chunk-CIURPPOW.js +0 -1
  119. package/dist/chunk-CIURPPOW.js.map +0 -1
  120. package/dist/chunk-IUOEGBF6.js +0 -49
  121. package/dist/chunk-IUOEGBF6.js.map +0 -1
  122. package/dist/chunk-NBMINJ5E.js +0 -230
  123. package/dist/chunk-NBMINJ5E.js.map +0 -1
  124. package/dist/chunk-O2RY2SWN.js +0 -1
  125. package/dist/chunk-O2RY2SWN.js.map +0 -1
  126. package/dist/chunk-PLMVKTQM.js +0 -1
  127. package/dist/chunk-PLMVKTQM.js.map +0 -1
  128. package/dist/chunk-RSTLW7XH.js +0 -130
  129. package/dist/chunk-RSTLW7XH.js.map +0 -1
  130. package/dist/chunk-SOBZ2FPP.js +0 -355
  131. package/dist/chunk-SOBZ2FPP.js.map +0 -1
  132. package/dist/chunk-VC5MKEY2.js +0 -170
  133. package/dist/chunk-VC5MKEY2.js.map +0 -1
  134. package/dist/chunk-W2GUAWUR.js +0 -147
  135. package/dist/chunk-W2GUAWUR.js.map +0 -1
  136. package/dist/chunk-Y2M3USWZ.js +0 -355
  137. package/dist/chunk-Y2M3USWZ.js.map +0 -1
  138. package/dist/chunk-Z4DJPESQ.js +0 -1216
  139. package/dist/chunk-Z4DJPESQ.js.map +0 -1
  140. package/dist/chunk-ZDM5VNWB.js +0 -1
  141. package/dist/chunk-ZDM5VNWB.js.map +0 -1
  142. package/dist/events/index.cjs +0 -820
  143. package/dist/events/index.cjs.map +0 -1
  144. package/dist/events/index.d.cts +0 -58
  145. package/dist/events/index.d.ts +0 -58
  146. package/dist/events/index.js +0 -69
  147. package/dist/events/index.js.map +0 -1
  148. package/dist/format-6RG4OcyE.d.ts +0 -57
  149. package/dist/format-B7d40f65.d.cts +0 -57
  150. package/dist/index.cjs +0 -4122
  151. package/dist/index.cjs.map +0 -1
  152. package/dist/index.d.cts +0 -21
  153. package/dist/index.d.ts +0 -21
  154. package/dist/index.js +0 -274
  155. package/dist/index.js.map +0 -1
  156. package/dist/logger-3phvV_fl.d.cts +0 -185
  157. package/dist/logger-CYza_0eU.d.ts +0 -185
  158. package/dist/logs-D7sYBvSy.d.cts +0 -66
  159. package/dist/logs-D7sYBvSy.d.ts +0 -66
  160. package/dist/notifiers/index.cjs +0 -620
  161. package/dist/notifiers/index.cjs.map +0 -1
  162. package/dist/notifiers/index.d.cts +0 -186
  163. package/dist/notifiers/index.d.ts +0 -186
  164. package/dist/notifiers/index.js +0 -70
  165. package/dist/notifiers/index.js.map +0 -1
  166. package/dist/pricing-BumgLpvB.d.cts +0 -79
  167. package/dist/pricing-BumgLpvB.d.ts +0 -79
  168. package/dist/ratelimit-BKVQG2oY.d.cts +0 -42
  169. package/dist/ratelimit-CGLlXNIS.d.ts +0 -42
  170. package/dist/registry-D3We8Lg6.d.ts +0 -50
  171. package/dist/registry-DePA3IyS.d.cts +0 -50
  172. package/dist/rules/index.cjs +0 -614
  173. package/dist/rules/index.cjs.map +0 -1
  174. package/dist/rules/index.d.cts +0 -166
  175. package/dist/rules/index.d.ts +0 -166
  176. package/dist/rules/index.js +0 -60
  177. package/dist/rules/index.js.map +0 -1
  178. package/dist/schema-DuDMPqpq.d.ts +0 -183
  179. package/dist/schema-NnIdKFl2.d.cts +0 -183
  180. package/dist/service/index.cjs +0 -3774
  181. package/dist/service/index.cjs.map +0 -1
  182. package/dist/service/index.d.cts +0 -262
  183. package/dist/service/index.d.ts +0 -262
  184. package/dist/service/index.js +0 -45
  185. package/dist/service/index.js.map +0 -1
  186. package/dist/service/main.cjs +0 -3759
  187. package/dist/service/main.cjs.map +0 -1
  188. package/dist/service/main.d.ts +0 -1
  189. package/dist/service/main.js +0 -60
  190. package/dist/service/main.js.map +0 -1
  191. package/dist/sources-CAaQR6-N.d.ts +0 -159
  192. package/dist/sources-CpDOUlzx.d.cts +0 -159
  193. package/dist/telegram-CpodYJrP.d.cts +0 -64
  194. package/dist/telegram-pvphmBwn.d.ts +0 -64
  195. package/dist/tiers/index.cjs +0 -471
  196. package/dist/tiers/index.cjs.map +0 -1
  197. package/dist/tiers/index.d.cts +0 -231
  198. package/dist/tiers/index.d.ts +0 -231
  199. package/dist/tiers/index.js +0 -34
  200. package/dist/tiers/index.js.map +0 -1
  201. package/dist/types-C0ETog04.d.ts +0 -109
  202. package/dist/types-CcoKpStA.d.cts +0 -109
  203. package/dist/types-DFPpTMWo.d.cts +0 -186
  204. package/dist/types-DFPpTMWo.d.ts +0 -186
package/README.md CHANGED
@@ -1,369 +1,313 @@
1
1
  # hood-alerts
2
2
 
3
- **Telegram and Discord alert bots for [Robinhood Chain](https://docs.robinhood.com/chain/) memecoins (chain ID 4663): new launches, graduations, whale trades. Hosted service, free and premium tier.**
3
+ [![license](https://img.shields.io/badge/license-MIT-93a1af?labelColor=101418)](./LICENSE)
4
4
 
5
- Chain watching is not this package's job. [`hoodchain`](https://github.com/nirholas/robinhood-chain-sdk) already has the launchpad watchers, the sequencer firehose and the Uniswap v3 quote path, and [`hoodkit`](https://github.com/nirholas/robinhood-chain-kit) has the swap decoding. hood-alerts is everything above them, which is where an alert product actually lives:
5
+ The alert layer for **Robinhood Chain** (chain ID 4663): Telegram and Discord bots, plus optional
6
+ X (Twitter) auto-posting, backed by one shared detection engine that watches launches, graduations,
7
+ whale trades, Stock Token price moves, on-chain premium/discount arbitrage, holder milestones, and
8
+ liquidity-pull rug warnings. Free tier plus x402 USDG premium.
6
9
 
7
- - a normalized **event taxonomy** over both launchpads, with honest USD values,
8
- - a schema-validated **rule engine** whose rules are data a subscriber can edit from chat,
9
- - **delivery adapters** that get escaping and rate limiting right on both platforms,
10
- - the **bot** command surface,
11
- - an enforced **tier policy**,
12
- - and a **hosted service** that survives a restart mid-stream without double-sending or silently skipping a block.
10
+ ## What it detects
13
11
 
14
- Docs: **https://nirholas.github.io/hood-alerts/**
12
+ | Detector | Topic | Source |
13
+ | --- | --- | --- |
14
+ | New token launch | `launches` (`launches:noxa`, `launches:odyssey`) | NOXA (instant Uniswap v3 pool) and The Odyssey (bonding curve) |
15
+ | Bonding-curve graduation | `graduations` | Odyssey curve fills and migrates to a locked Uniswap v3 pool |
16
+ | Whale trades | `whales`, or per-token `token:0x…` | Odyssey curve trades and Uniswap v3 swaps, chain-wide, threshold in USD |
17
+ | Stock Token price moves | `stock:SYMBOL` | Chainlink feed, rolling window, signed percent change |
18
+ | On-chain premium/discount (the arb signal) | `premiums` (premium tier) | DEX mid vs Chainlink feed, hysteresis ladder at 1/2/3/5/10/20/50% |
19
+ | Holder milestones | per-token `token:0x…` | Blockscout `token_holders_count`, crossing 10 through 100,000 |
20
+ | Liquidity-pull rug warning | `rugs` (premium tier), or per-token `token:0x…` | tracked pool's quote-side (USDG/WETH) reserves dropping sharply |
15
21
 
16
- ## The thing most memecoin alert bots get wrong
22
+ Every event is deduped per fingerprint before it reaches a subscriber, routed through a
23
+ premium/quiet-hours/digest gate, and rendered as a platform-native card (Telegram HTML, Discord
24
+ embed, a compact X post, or a structured console log line) with links back to the chain explorer
25
+ and `three.ws/markets/robinhood`.
17
26
 
18
- Robinhood Chain has two memecoin launchpads and they do not have the same lifecycle. Treating them as one product produces alerts that are simply false.
27
+ ## Quickstart (self-host)
19
28
 
20
- | | NOXA (`fun.noxa.fi/robinhood`) | The Odyssey (`theodyssey.fun`) |
21
- |---|---|---|
22
- | Model | Instant launcher | pump.fun-style bonding curve with virtual reserves |
23
- | At launch | Deploys the ERC-20, creates a Uniswap v3 pool, seeds single-sided liquidity and locks the LP NFT, all in one transaction | Opens a curve. No pool exists yet |
24
- | Trading | Normal Uniswap v3 swaps from block one | On the curve until it fills |
25
- | Graduation | **Never happens. There is no curve to fill** | `PoolCompleted` + `PoolMigrated` when the curve fills and liquidity moves to a locked Uniswap v3 pool |
26
- | Emits | `launch`, `whale_trade` | `launch`, `curve_trade`, `graduation`, `whale_trade` |
27
-
28
- A "NOXA graduation" alert would describe an event that does not exist on chain. hood-alerts encodes that asymmetry in the types and in the rule schema, so an impossible rule is **rejected at validation time** instead of being accepted and never firing:
29
-
30
- ```ts
31
- import { safeParseRule } from 'hood-alerts/rules'
32
-
33
- const result = safeParseRule({ id: 'nope', kinds: ['graduation'], launchpads: ['noxa'] })
34
- console.log(result.success)
35
- // false: "this combination can never fire: NOXA is an instant launcher with no
36
- // bonding curve, so it emits no curve_trade and no graduation"
29
+ ```sh
30
+ npm install
31
+ npm run dev # tsx watch, console transport only if no bot tokens are set
37
32
  ```
38
33
 
39
- ## Event taxonomy
40
-
41
- Every event carries a stable id (`kind:txHash:logIndex`), the token, the actor, the block, the transaction hash, a USD value and explorer links.
34
+ With zero environment variables the engine still runs: detectors connect to the public Robinhood
35
+ Chain RPC, alerts print as structured JSON log lines (the console transport), and `/healthz` comes
36
+ up on `:8080`. That is the self-host smoke-test mode. Copy `.env.example` to `.env` and fill in a
37
+ Telegram and/or Discord bot token to go live on those platforms; see `.env.example` for every
38
+ variable, defaults, and what each detector threshold controls.
42
39
 
43
- | Kind | Source | Launchpads | `usdValue` is | Extra fields |
44
- |---|---|---|---|---|
45
- | `launch` | NOXA `TokenLaunched`, Odyssey `TokenCreated` | both | the deployer's initial buy (NOXA), or `null` (Odyssey: the curve holds the liquidity) | `pool`, `pairToken`, `initialBuyAmount`, `positionId`, `instantListing` |
46
- | `curve_trade` | Odyssey `Traded` on a bonding-curve factory | Odyssey only | the native-ETH leg, priced through live WETH/USDG liquidity | `side`, `tokenAmount`, `quoteAmountWei`, `feeWei`, `virtualQuoteWei`, `virtualTokenAmount`, `priceEth` |
47
- | `graduation` | Odyssey `PoolMigrated` | Odyssey only | the quote side used to seed the migrated pool | `pool`, `positionId`, `liquidity`, `tokenUsed`, `quoteUsed` |
48
- | `whale_trade` | Uniswap v3 `Swap` on a tracked memecoin pool | both | the swap's quote leg | `pool`, `quoteToken`, `quoteSymbol`, `side`, `tokenAmount`, `quoteAmount`, `price`, `feeTier` |
49
-
50
- ### USD values are measured, never assumed
51
-
52
- There is no hardcoded price anywhere in this package.
53
-
54
- - **USDG legs are the unit of account.** USDG is the chain's fully reserved dollar stablecoin at 6 decimals, so a USDG leg is its own USD value.
55
- - **ETH legs are priced through the chain's own liquidity.** `hoodchain`'s `quoteSwap` is asked for the real output of selling 1 WETH into USDG across every fee tier and two-hop route, cached for 30 seconds and coalesced so a burst of events makes one quote.
56
- - **Anything else is `null`.** A memecoin/memecoin pool has no honest USD value from chain data alone, so the event says so and a `minUsd` rule does not match it. Unknown is never treated as zero (which would satisfy every `maxUsd` filter) or as infinity.
57
-
58
- ### Evidence: every source decoded against mainnet
40
+ ```sh
41
+ npm run build # tsc -> dist/
42
+ npm start # node dist/src/index.js
43
+ ```
59
44
 
60
- `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:
45
+ ### Creating the bots
46
+
47
+ - **Telegram**: message [@BotFather](https://t.me/BotFather), `/newbot`, paste the token into
48
+ `HOOD_ALERTS_TELEGRAM_TOKEN`. The bot registers its command list on startup.
49
+ - **Discord**: create an application at the
50
+ [Developer Portal](https://discord.com/developers/applications), add a bot, copy the bot token
51
+ into `HOOD_ALERTS_DISCORD_TOKEN` and the application id into `HOOD_ALERTS_DISCORD_APP_ID`. Slash
52
+ commands register (guild-visible immediately) on startup.
53
+
54
+ Leaving either token unset disables that transport; the engine and the console transport keep
55
+ running regardless.
56
+
57
+ ### X (Twitter)
58
+
59
+ X is a broadcast-only transport: there is no inbound bot, so you cannot DM the account and get a
60
+ reply the way you can with Telegram or Discord. What it auto-posts is fixed at startup by
61
+ `HOOD_ALERTS_X_TOPICS` (default `launches,graduations,whales`) instead of `watch`/`unwatch`
62
+ commands, parsed through the same topic resolver the bots use. Pick exactly **one** of two modes
63
+ via `HOOD_ALERTS_X_MODE`; see `.env.example` for the full variable list.
64
+
65
+ **`official`**: the real X API v2, `POST /2/tweets`, signed with your own developer app's OAuth1
66
+ user-context credentials. This is the normal, ToS-compliant path, and it costs money: X gates
67
+ posting behind a paid API tier. Check current pricing at
68
+ [developer.x.com/en/portal/products/buy](https://developer.x.com/en/portal/products/buy) before
69
+ relying on it; it has changed before and can change again.
70
+
71
+ Setup: create an app at the [developer portal](https://developer.x.com/en/portal), generate a
72
+ consumer key/secret (`HOOD_ALERTS_X_API_KEY` / `HOOD_ALERTS_X_API_SECRET`), then generate an
73
+ access token/secret with **read and write** permissions (`HOOD_ALERTS_X_ACCESS_TOKEN` /
74
+ `HOOD_ALERTS_X_ACCESS_SECRET`). All four are required; hood-alerts signs every request itself with
75
+ Node's built-in `crypto` (HMAC-SHA1 per RFC 5849), no extra dependency.
76
+
77
+ **`xactions`**: posts through a self-hosted [XActions](https://github.com/nirholas/XActions)
78
+ instance instead of the official API: browser-automation driving your own X session cookie. Free,
79
+ no developer account, no paid tier. This is **not** the official API and carries real ToS risk;
80
+ run it on an account you are prepared to lose if X acts on that. Point `HOOD_ALERTS_XACTIONS_URL`
81
+ at your running instance and `HOOD_ALERTS_XACTIONS_TOKEN` at its bearer token. Posting is
82
+ async/job-queued: hood-alerts treats a 2xx from `POST {url}/api/posting/tweet` as "accepted", not
83
+ "delivered", and logs that delivery is not confirmed synchronously (the real xactions API responds
84
+ `{ operationId, status: "queued" }`, not a tweet id).
85
+
86
+ Both modes fail loudly: a configured mode missing any required credential logs a warning and
87
+ disables the X transport (Telegram, Discord, and the console transport keep running) instead of
88
+ crashing the process. Every X post is compacted to fit X's 280-character limit: title, the single
89
+ most important line, and the first link; only the text portion truncates, never the URL. Digests
90
+ (too many alerts to post individually) post the single most significant event plus a `(+N more)`
91
+ count, and log the full summarized batch, so nothing is silently dropped from the record even
92
+ though it is dropped from the post itself.
93
+
94
+ ## Bot commands
95
+
96
+ Both bots share one command router (`src/commands/commands.ts`), so the UX is identical modulo
97
+ native slash-command vs `/command arg` syntax:
61
98
 
62
99
  ```
63
- $ FROM_BLOCK=61688 TO_BLOCK=1500000 npm run verify:chain
64
-
65
- chain 4663, head block 15010850
66
- ETH/USD from live Uniswap v3 liquidity: $1905.26
67
-
68
- scanning blocks 61688 to 1500000 (1438313 blocks)
69
-
70
- NOXA launches 2621 events 217029ms
71
- launch ? 0x6399E2Bd8af62C0ac13f55613C3469b67332a6Fd $266.74 block 61869
72
- launch HUSK 0x57EB9C9153cfE0277F91Ff8B8604C2D3006a9196 $9.53 block 61987
73
- launch JOHN 0x1E963A1539681d0B877570F8bdC000cbE8404fC4 $95.26 block 62204
74
- Odyssey launches 4 events 757ms
75
- launch ROBIN 0xfB4729659eeF22Bfc1c2B680F6F873f8147aaaab unpriced block 983265
76
- Odyssey curve trades 110 events 2482ms
77
- curve_trade ROBIN 0xfB4729659eeF22Bfc1c2B680F6F873f8147aaaab $18.67 block 983265
78
- Odyssey graduations 1 events 344ms
79
- graduation ROBIN 0xfB4729659eeF22Bfc1c2B680F6F873f8147aaaab $7618.47 block 1048638
80
- Whale trades 4045 events 83886ms
81
- whale_trade CHEEMS 0xdaA213A0Bd8B048D6022e2c46df877E8A204072b $190.46 block 1439024
82
-
83
- pools discovered and registered for whale watching: 2622
100
+ watch <what> [threshold]
101
+ launches every new token (add noxa/odyssey to filter)
102
+ graduations Odyssey curves migrating to Uniswap v3
103
+ whales [usd] trades >= usd, chain-wide (default 5000)
104
+ premiums [pct] Stock Token DEX premium/discount vs Chainlink (premium tier)
105
+ rugs [pct] liquidity-pull rug warnings (premium tier)
106
+ <TICKER> a Stock Token: price moves + premium crossings (e.g. TSLA)
107
+ <0x address> a memecoin: whale trades, graduation, holders, liquidity
108
+
109
+ unwatch <what|all> stop watching
110
+ list your subscriptions
111
+ threshold <what> <n> change a subscription's threshold
112
+ digest on|off [min] batch alerts into digests (default 60 min)
113
+ quiet <from> <to> quiet hours in UTC (e.g. quiet 22 7); quiet off
114
+ premium premium status + how to upgrade
115
+ help this message
84
116
  ```
85
117
 
86
- Read the ROBIN token across those four lines and the taxonomy proves itself: a curve opens (`launch`, unpriced, because a curve holds no pool), trades on the curve (`curve_trade`, priced through the ETH leg), then fills and migrates (`graduation`, $7,618.47 of liquidity seeded). NOXA tokens never appear in the middle two rows, because they cannot. The first NOXA launch has no `symbol()` and renders as `?` rather than dropping the alert.
118
+ In a group chat or server, `watch`, `unwatch`, `threshold`, `digest`, and `quiet` are restricted to
119
+ admins; `list`, `premium`, and `help` are open to everyone.
87
120
 
88
- The whale row also shows the pool set working: 2,622 pools were discovered from launchpad activity during the same scan, and the whale watcher queried exactly those.
121
+ ## Free vs premium
89
122
 
90
- ## Install
91
-
92
- ```bash
93
- npm install hood-alerts hoodchain hoodkit viem
94
- ```
123
+ | | Free | Premium |
124
+ | --- | --- | --- |
125
+ | Subscriptions | 3 | Unlimited |
126
+ | Delivery | Batched to at most one immediate alert per 60s; extras fold into a digest | Real-time, subject only to quiet hours / digest mode you set |
127
+ | `premiums` (Stock Token arb signal) | Not available | Included |
128
+ | `rugs` (liquidity-pull warning) | Not available | Included |
129
+ | Price | Free | `HOOD_ALERTS_PREMIUM_PRICE_USDG` (default 5) USDG for `HOOD_ALERTS_PREMIUM_DAYS` (default 30) days |
95
130
 
96
- Node >= 20. `hoodchain`, `hoodkit` and `viem` are peer dependencies.
131
+ Premium is paid on Robinhood Chain via **x402** using [`hood402`](../hood402) (USDG,
132
+ EIP-3009 `transferWithAuthorization`, chain 4663). The bot's `premium` command hands back a
133
+ `POST /premium/activate?platform=...&chat=...` URL: the first request gets a 402 challenge with
134
+ payment instructions, and any x402 client (`hood402`, `x402-fetch`) that can sign a USDG
135
+ authorization completes the purchase. The entitlement never activates before the transfer settles
136
+ on-chain, so a signature alone buys nothing.
97
137
 
98
- ## Quickstart: the event pipeline
138
+ Purchases stay disabled (`503` + setup instructions) until the instance sets `HOOD402_PAY_TO` and
139
+ either `HOOD402_FACILITATOR_URL` (delegates verification and settlement, no gas key on this box) or
140
+ `HOOD402_SETTLER_KEY` (this instance broadcasts the settlement itself). See `.env.example` for both
141
+ modes.
99
142
 
100
- ```ts
101
- import { createHoodClient } from 'hoodchain'
102
- import {
103
- createEventSources,
104
- createMemoryPoolRegistry,
105
- createPriceOracle,
106
- createTokenMetaReader,
107
- } from 'hood-alerts/events'
108
-
109
- const hood = createHoodClient()
110
- const sources = createEventSources({
111
- client: hood,
112
- oracle: createPriceOracle(hood),
113
- tokens: createTokenMetaReader(hood),
114
- registry: createMemoryPoolRegistry(),
115
- })
116
-
117
- const head = await hood.public.getBlockNumber()
118
- for (const source of sources) {
119
- for (const event of await source.poll(head - 5_000n, head - 2n)) {
120
- console.log(event.kind, event.symbol, event.usdValue, event.explorer.tx)
121
- }
122
- }
123
- ```
143
+ ## HTTP API
124
144
 
125
- ## Quickstart: rules and delivery
145
+ The same process that runs the bots serves a small Hono app on `PORT` (default 8080):
126
146
 
127
- ```ts
128
- import { matchRules, parseRule } from 'hood-alerts/rules'
129
- import { createTelegramNotifier, renderAlert } from 'hood-alerts/notifiers'
147
+ | Endpoint | Method | Returns |
148
+ | --- | --- | --- |
149
+ | `/` | GET | service metadata and endpoint list |
150
+ | `/healthz` | GET | uptime, last detector event time, per-event-type counts, whether premium purchases are enabled |
151
+ | `/premium/status?platform=telegram\|discord\|console\|x&chat=<id>` | GET | `{ tier: 'free' \| 'premium', expiresAt }` for that chat |
152
+ | `/premium/activate?platform=...&chat=...` | POST | the x402 purchase flow: 402 challenge, then 200 + entitlement once payment settles |
130
153
 
131
- const rules = [
132
- parseRule({ id: 'whales', name: 'Whale buys over $25k', kinds: ['whale_trade'], minUsd: 25_000, side: 'buy' }),
133
- parseRule({ id: 'grads', kinds: ['graduation'], launchpads: ['odyssey'] }),
134
- ]
154
+ ## Example: reading the alert engine's pure logic directly
135
155
 
136
- const telegram = createTelegramNotifier({ botToken: process.env.TELEGRAM_BOT_TOKEN as string })
156
+ `src/engine/gate.ts` and `src/engine/topics.ts` have no I/O and are safe to import standalone, for
157
+ example to preview how a threshold will gate before wiring a subscription:
137
158
 
138
- for (const { rule } of await matchRules(rules, event)) {
139
- const result = await telegram.send('-1001234567890', renderAlert(event, { footer: `rule: ${rule.id}` }))
140
- if (!result.ok) console.error(result.error, 'retryable:', result.retryable)
159
+ ```ts
160
+ import { gate } from './src/engine/gate.js'
161
+ import { resolveWatchTarget, parseTopic } from './src/engine/topics.js'
162
+
163
+ const target = resolveWatchTarget('whales') // { ok: true, topic: 'whales' }
164
+ if (target.ok) {
165
+ const topic = parseTopic(target.topic) // { kind: 'whales' }
166
+ const decision = gate(
167
+ { premium: false, digest: false, quietStart: null, quietEnd: null, lastDeliveredAt: null, deliveredLastMinute: 0 },
168
+ Math.floor(Date.now() / 1000),
169
+ )
170
+ console.log(topic, decision) // { kind: 'whales' } { action: 'deliver' }
141
171
  }
142
172
  ```
143
173
 
144
- To see the exact bytes each platform would receive, with real chain events and no credentials:
145
-
146
- ```bash
147
- npm run demo:notify
148
- ```
149
-
150
- ## Rules are data
151
-
152
- A rule is a JSON document validated by a [Zod](https://zod.dev) schema, so it can be stored per subscriber, edited from a chat command, exported and diffed. The schema is the single source of truth: the bot, the service and the tier policy all validate through it.
153
-
154
- | Field | Type | Filter |
155
- |---|---|---|
156
- | `id` | slug | Stable id, unique per subscription |
157
- | `name` | string | Label shown in `/rules` |
158
- | `enabled` | boolean | Off without deleting |
159
- | `kinds` | `launch` / `curve_trade` / `graduation` / `whale_trade` | Which events |
160
- | `launchpads` | `noxa` / `odyssey` | Which launchpad |
161
- | `minUsd`, `maxUsd` | number | USD value band. An unpriced event never matches |
162
- | `minLiquidityUsd`, `maxLiquidityUsd` | number | Deepest known pool's USD reserves |
163
- | `tokens` | address[] | Watchlist. Non-empty means only these tokens |
164
- | `deployers` | address[] | Only these actors |
165
- | `excludeDeployers` | address[] | Never these actors |
166
- | `side` | `buy` / `sell` / `any` | Trade direction (trades only) |
167
- | `reputation.minPriorLaunches` | int | Deployer's prior launches as of the event's block |
168
- | `reputation.maxPriorLaunches` | int | Rejects serial deployers |
169
- | `reputation.maxRuggedLaunches` | int | Rejects deployers with drained pools |
170
- | `reputation.requireLpLocked` | boolean | LP NFT held by the launchpad locker |
171
- | `rateLimit.maxPerHour` | int | Cap per rolling hour |
172
- | `rateLimit.minIntervalSeconds` | int | Minimum gap between alerts |
173
-
174
- Evaluation is lazy and ordered: cheap in-memory filters run before anything that costs an RPC call, so a rule that rejects an event on its kind never triggers a pool balance read. With thousands of subscriptions that is the difference between keeping up with the chain and not.
175
-
176
- ### Deployer reputation is derived on chain
177
-
178
- Nothing is scraped, self-reported or scored by a model.
179
-
180
- - **Prior launches**: `TokenLaunched` logs on the NOXA factory indexed by `deployer`, plus `TokenCreated` on the three Odyssey factories indexed by `creator`. Both queries are topic-selective, so the RPC serves the full chain history in one call each. Counted relative to the event's own block, so replaying a historical range gives the same answer twice.
181
- - **LP locked**: `NonfungiblePositionManager.ownerOf(positionId)` equals the NOXA locker contract. That is the launchpad's own permanent-lock mechanism, read directly rather than trusted from a UI badge.
182
- - **Drained**: a prior launch whose pool quote reserve is now below the rug threshold (default $50). This is a **heuristic** and is labelled as one everywhere it appears: a token that never traded and a token whose liquidity was pulled both end up with an empty pool. Pair it with `minPriorLaunches` for a meaningful signal.
183
-
184
- ```bash
185
- npx tsx examples/deployer-reputation.ts 0xYourDeployerAddress
186
- ```
187
-
188
- ## Escaping, because that is how alert bots die
189
-
190
- Memecoin names are adversarial input by nature. A token called `WHO_LET_THE` italicises half a MarkdownV2 message; a token called `<b>RUG` injects markup under HTML parse mode. Either way Telegram answers `400 Bad Request: can't parse entities` and the alert is lost.
191
-
192
- Each context gets its own escaper and its own tests, because the rules are not symmetric even within one platform:
193
-
194
- | Context | Escaped |
195
- |---|---|
196
- | Telegram MarkdownV2 text | all 18 of ``_*[]()~`>#+-=\|{}.!`` |
197
- | MarkdownV2 link URL | only `)` and `\` (escaping the full set corrupts query strings) |
198
- | MarkdownV2 code span | only `` ` `` and `\` |
199
- | Telegram HTML text | `&`, `<`, `>` |
200
- | Telegram HTML attribute | the above plus `"` |
201
- | Discord embeds | ``\*_~`\|>[]()`` |
174
+ A threshold typed after the target (`watch whales 10000`) is parsed separately by
175
+ `Commands.watch()`, not by `resolveWatchTarget` itself.
202
176
 
203
- Telegram delivery defaults to HTML: three special characters instead of eighteen makes a malformed-entity 400 far less likely on hostile input. Truncation is surrogate-pair aware, so a long emoji-bearing name is cut cleanly instead of ending in a replacement character.
177
+ The package does not ship a public library entry point today (`main`/`bin` both point at the CLI
178
+ process in `src/index.ts`, which starts the bots and the HTTP server and does not export anything);
179
+ importing from `src/` as above works in this checkout but is not a published API contract.
204
180
 
205
- ## Rate limiting, per each platform's actual contract
181
+ ## Configuration
206
182
 
207
- The two platforms do not work the same way, and treating them the same is how a bot gets throttled into silence.
183
+ Every variable, its default, and what it controls: [`.env.example`](.env.example). Highlights:
208
184
 
209
- - **Telegram** answers `429` with `parameters.retry_after` in **whole seconds** and expects exactly that wait. hood-alerts honours it (falling back to the `Retry-After` header, then to exponential backoff), caps the total wait at `maxWaitMs` so one throttled chat cannot stall the dispatch loop, and treats `400`/`403` as permanent so a deleted chat or a blocked bot stops being retried forever.
210
- - **Discord** publishes a per-route bucket on every response (`X-RateLimit-Remaining`, `X-RateLimit-Reset-After` in **fractional seconds**). The correct behaviour is to not send once a bucket is exhausted rather than to send and spend a 429, so the adapter waits before the request. A 429 that still slips through carries `retry_after` as a float, and `X-RateLimit-Global` is applied to every route rather than only the one that hit it.
185
+ - `HOOD_ALERTS_RPC_URL` custom RPC; defaults to viem's public `robinhood` chain RPC.
186
+ - `HOOD_ALERTS_DB` SQLite path (subscriptions, entitlements, dedup, delivery log). `:memory:`
187
+ works for tests.
188
+ - `HOOD_ALERTS_WHALE_FLOOR_USD` / `HOOD_ALERTS_WHALE_DEFAULT_USD` — engine-side emission floor vs.
189
+ the default per-subscription threshold.
190
+ - `HOOD_ALERTS_PRICE_WINDOW_S` / `HOOD_ALERTS_PRICE_DEFAULT_PCT` — Chainlink rolling-window move
191
+ detector.
192
+ - `HOOD_ALERTS_PREMIUM_POLL_S` / `HOOD_ALERTS_PREMIUM_DEFAULT_PCT` — Stock Token arb poll cadence
193
+ and default ladder entry.
194
+ - `HOOD_ALERTS_RUG_DEFAULT_PCT` — default liquidity-pull threshold (percent of quote reserves).
211
195
 
212
- ## The service
213
-
214
- One process: poll the chain, match rules, queue deliveries, drain the queue, serve health and metrics.
196
+ ## Development
215
197
 
216
- ```bash
217
- cp .env.example .env # nothing is required for a dry run
198
+ ```sh
218
199
  npm install
219
- npm run build
220
- DRY_RUN=1 npm start
200
+ npm run typecheck # tsc --noEmit, strict + exactOptionalPropertyTypes
201
+ npm test # vitest: dedup/rate-limit gate, digest batching, topic parsing and
202
+ # matching, event fingerprinting/TTL, alert card rendering, config
203
+ # loading, the command router, and the subscriber/entitlement/delivery
204
+ # repositories — all offline, no chain or bot credentials required
205
+ npm run build # tsc -> dist/
221
206
  ```
222
207
 
223
- ```bash
224
- docker build -t hood-alerts .
225
- docker run --rm -p 8080:8080 -v hood-alerts-data:/app/data \
226
- -e TELEGRAM_BOT_TOKEN=123456789:AA... hood-alerts
227
- ```
208
+ The `whale-classify` and `detectors` suites run against **real captured mainnet event streams** in
209
+ `tests/fixtures/` (real Uniswap v3 swaps, Odyssey bonding-curve trades, and Chainlink prices).
210
+ Refresh them any time with `npm run capture-fixtures`, which re-reads live chain 4663.
228
211
 
229
- | Endpoint | Purpose |
230
- |---|---|
231
- | `GET /health` | Liveness, head block, last poll, configured platforms |
232
- | `GET /ready` | 503 when the poll loop has stalled, so an orchestrator restarts a process that is alive but no longer ingesting |
233
- | `GET /metrics` | Prometheus text: subscriptions, outbox by status, deliveries per hour, tracked pools, per-source cursor and lag |
234
- | `POST /discord/interactions` | Discord slash commands (Ed25519 verified) |
212
+ `npm run test:live` and `npm run probe` exercise the live detector set against real Robinhood Chain
213
+ RPC and, for `probe`, the console transport; they need real network access and are not part of the
214
+ offline unit suite above. `npm run grant-premium` comps or inspects a premium entitlement directly
215
+ in the database (handy for testing premium delivery before a payment rail is wired).
235
216
 
236
- ### Surviving a restart mid-stream
217
+ ### Local sibling packages
237
218
 
238
- This is the part that is easy to get wrong and expensive to get wrong: an alert bot that double-sends is spam, and one that skips a range misses the launch its subscribers paid for.
219
+ `hood402`, `hoodkit`, and `hoodchain` are consumed as `file:` dependencies of the other Robinhood
220
+ Chain repos in this workspace (`../hood402`, `../hoodkit`, `../robinhood-chain-sdk`) rather than
221
+ their published npm versions, so a change to any of them is picked up on the next `npm install`
222
+ here. A `file:` link keeps each sibling's own `viem` copy, so a patch-level skew (e.g. `2.55.2`
223
+ here vs `2.55.1` in `hood402`) makes viem's structural `Chain` / `WalletClient` types diverge across
224
+ the two copies. `src/premium/paywall.ts` bridges that boundary explicitly (casting the local viem
225
+ clients to `hood402`'s `HoodBroadcaster` / `HoodConfirmer`), so the build stays green across a patch
226
+ skew; the runtime objects are the same correct viem clients. Once the siblings are on npm and a
227
+ single `viem` resolves, the cast is a no-op.
239
228
 
240
- 1. **Never double-send.** Every potential delivery has a deterministic primary key, `eventId|subscriptionId|ruleId`, and the event id comes from the transaction hash and log index. Re-processing a block range regenerates identical keys, so the second pass inserts nothing.
241
- 2. **Never silently skip.** The block cursor advances only after every event in a chunk has been enqueued and committed. A crash mid-chunk leaves the cursor at the start of that chunk, so the range is re-read. A failed RPC call leaves it untouched for the same reason.
242
- 3. **Never lose an enqueued alert.** Queued rows live in an outbox until they are delivered or dead-lettered. On startup, rows left in `sending` (a crash mid-flight) return to `pending` and are retried.
229
+ ## Deploy
243
230
 
244
- The one honest caveat: if the process dies after the platform accepted a message but before the row was marked `sent`, that alert goes out twice. Neither the Telegram nor the Discord send API takes a client-supplied idempotency key, so that window cannot be closed from here. Everything outside it is exactly once.
231
+ hood-alerts is one long-running process (engine + HTTP server + both bots), so run it as a service
232
+ with a warm instance, not a job.
245
233
 
246
- All three properties are tested, including a simulated crash part way through writing a batch:
234
+ Because the sibling packages are `file:` links, the Docker build context is the **parent
235
+ `robinhood/` directory** so they resolve:
247
236
 
248
- ```
249
- cursor semantics > leaves the cursor untouched when a source throws, so nothing is skipped
250
- dedupe > enqueues an event once even when the same range is processed twice
251
- ✓ crash recovery > re-processes only the unfinished range after a crash mid-batch
252
- ✓ crash recovery > returns rows abandoned in flight to the queue on restart
253
- ```
237
+ ```sh
238
+ # from robinhood/ (the parent of hood-alerts/)
239
+ docker build -f hood-alerts/Dockerfile -t hood-alerts .
254
240
 
255
- ## Bot command reference
256
-
257
- The same router serves Telegram (long polling, so no public URL, TLS certificate or inbound firewall rule is needed) and Discord (slash commands over the HTTP interactions endpoint). Every command answers bad input with a specific, actionable message.
258
-
259
- | Command | What it does |
260
- |---|---|
261
- | `/start`, `/help` | Every command, plus the launchpad lifecycle explanation |
262
- | `/subscribe [target]` | Send alerts here with a starter rule set. On Discord the target may be a webhook URL or a channel id; it defaults to the current channel |
263
- | `/unsubscribe [id]` | Stop a subscription. Lists them when there is more than one |
264
- | `/rules` | Every rule on every subscription, with its filters |
265
- | `/rule add {json}` | Add a rule. Validation errors come back as the field that failed |
266
- | `/rule rm\|on\|off <id>` | Remove, enable or disable a rule |
267
- | `/threshold <rule> <usd>` | Set a rule's minimum USD value |
268
- | `/watch <token> [rule]` | Add a token to a rule's watchlist |
269
- | `/unwatch <token> [rule]` | Remove one |
270
- | `/tier` | Your tier, its limits, and where the entitlement came from |
271
- | `/status` | Subscriptions, rule counts, alerts used this hour |
272
- | `/link [address] [signature]` | Link the wallet that pays. Send it bare to get the message to sign |
273
- | `/upgrade` | How to go premium on this deployment |
274
-
275
- Register the Discord slash commands with:
276
-
277
- ```bash
278
- DISCORD_APPLICATION_ID=... DISCORD_BOT_TOKEN=... npm run register:discord
241
+ gcloud run deploy hood-alerts \
242
+ --image=REGION-docker.pkg.dev/PROJECT/repo/hood-alerts:latest \
243
+ --min-instances=1 --no-cpu-throttling --port=8080 \
244
+ --set-env-vars=HOOD_ALERTS_TELEGRAM_TOKEN=…,HOOD_ALERTS_DISCORD_TOKEN=…,HOOD_ALERTS_DISCORD_APP_ID=…
279
245
  ```
280
246
 
281
- [INTEGRATION.md](./INTEGRATION.md) has the full Telegram and Discord setup, including the interactions endpoint.
247
+ Mount a volume at `/data` (the default `HOOD_ALERTS_DB` path) so subscriptions, entitlements, and
248
+ the delivery log survive restarts. The container answers `GET /healthz`; `SIGTERM` flushes pending
249
+ digests before exit. Once the siblings are published to npm, switch the `file:` specs in
250
+ `package.json` and the image builds from the `hood-alerts/` directory alone. Full self-host and
251
+ premium-wiring guide: the `docs/` site.
282
252
 
283
- ## Tiers are enforced, not advertised
253
+ ## Docs site (GitHub Pages)
284
254
 
285
- Every limit maps to a real cost the service pays, and the check lives in exactly one place (`createEntitlementGate`). The bot asks it before accepting a subscription or 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.
255
+ `docs/` is a static, hand-built site (no framework, no build step): open `docs/index.html` locally,
256
+ or serve it from GitHub Pages. Its landing page renders a **live** premium/discount feed client-side
257
+ straight from the public RPC (real Chainlink feeds vs real DEX pools, no server). One-time setup:
258
+ Settings -> Pages -> Deploy from a branch -> `main` -> `/docs`.
286
259
 
287
- | | Free | Premium |
288
- |---|---|---|
289
- | Subscriptions | 1 | 10 |
290
- | Rules per subscription | 3 | 50 |
291
- | Filters per rule | 3 | 24 |
292
- | Watchlist size | 5 | 500 |
293
- | Alerts per hour | 30 | 2,000 |
294
- | Delivery delay | 60s | none |
295
- | Liquidity and reputation filters | no | yes |
296
- | Event kinds | `launch`, `graduation`, `whale_trade` | all four, including the `curve_trade` firehose |
297
-
298
- The delivery delay is enforced in the outbox (the row carries `not_before`), so it survives a restart instead of being a `setTimeout` a crash erases.
299
-
300
- ### Entitlements: what actually ships
260
+ ## Architecture notes
301
261
 
302
- `EntitlementProvider` is a one-method interface and two implementations ship. **Both work; neither is a placeholder.**
262
+ - **Console transport is not a fallback, it's the smoke-test mode.** With no bot tokens configured
263
+ the engine still ingests real chain events and logs every alert/digest as structured JSON; that
264
+ is how a fresh self-host proves the pipeline works before touching Telegram or Discord.
265
+ - **The entitlement never activates before settlement.** `PremiumPaywall.activate()` calls
266
+ `hood402`'s verify-then-settle flow and only writes the entitlement row after the on-chain
267
+ transfer succeeds.
268
+ - **Digest buffering is in-memory by design.** A restart loses at most one pending batch, never a
269
+ subscription (subscriptions and the delivery log are in SQLite); `AlertEngine.stop()` flushes
270
+ every buffered digest before shutdown.
271
+ - **Auto-tracked pools expire; watched pools do not.** A NOXA/Odyssey launch or graduation starts a
272
+ 24h rug-watch on its pool automatically; a user `watch 0x…` keeps that pool's liquidity monitor
273
+ running indefinitely.
303
274
 
304
- 1. **`createStaticEntitlementProvider` is the documented default.** Premium comes from `PREMIUM_SUBSCRIBERS` (or `ALL_PREMIUM=1` for a private deployment). No dependencies, works offline, and is what a self-hoster running the bot for their own community wants.
305
- 2. **`createUsdgEntitlementProvider` is a working on-chain rail.** It reads USDG `Transfer` logs from a subscriber's linked wallet to the operator's `USDG_RECEIVER` and accrues subscription time from those real payments: each payment buys `floor(amount / price)` periods, starting from whenever the current entitlement would have lapsed, so renewing early extends rather than overwrites. No facilitator, no card processor, no third-party service. USDG is the chain's own dollar and the ledger is the chain.
275
+ ## Repo layout
306
276
 
307
- Wallet linking is signature-verified (EIP-191 `personal_sign` over a server-issued, single-use nonce), so nobody inherits a paying subscriber's entitlement by pasting their address.
308
-
309
- ```env
310
- ENTITLEMENTS=both
311
- USDG_RECEIVER=0xYourReceivingAddress
312
- PREMIUM_PRICE_USDG=25
313
- PREMIUM_PERIOD_DAYS=30
314
- PAYMENTS_FROM_BLOCK=12000000
277
+ ```
278
+ src/index.ts CLI entry: wires config, db, engine, detectors, transports, HTTP server
279
+ src/config.ts environment -> validated Config
280
+ src/server.ts Hono app: /, /healthz, /premium/status, /premium/activate
281
+ src/engine/ event types, dedup fingerprinting, topic parsing/matching, the
282
+ digest buffer, the delivery gate, and the live detector set
283
+ src/engine/detectors/ pure per-signal detectors (price move, premium ladder, holder
284
+ milestones, liquidity pull, whale classification) plus live.ts,
285
+ which wires them to hoodchain/hoodkit streams
286
+ src/db/ better-sqlite3 repositories: subscribers, entitlements, deliveries
287
+ src/premium/paywall.ts the hood402 x402 purchase flow
288
+ src/transports/ Telegram (grammY), Discord (discord.js), X (OAuth1 API v2 or self-
289
+ hosted xactions), and console transports
290
+ src/commands/commands.ts the shared, platform-neutral command router
291
+ src/format/cards.ts alert -> platform-neutral card -> per-platform rendering
292
+ tests/ vitest unit suite + tests/fixtures (real captured chain events) +
293
+ tests/live (opt-in on-chain reads)
294
+ scripts/ capture-fixtures, live-probe, grant-premium (see Development above)
295
+ docs/ static GitHub Pages site with a live client-side premium feed
296
+ Dockerfile one-container build (engine + both bots), build from robinhood/
315
297
  ```
316
298
 
317
- A deliberate non-choice: hood-alerts does **not** wire the sibling `hood402` x402 rail. x402 prices a single HTTP request, and a subscription is not a request. Bolting one onto the other would have produced a payment integration that demos but does not work. The USDG provider is the honest version of the same idea, and `EntitlementProvider` is one method wide for anyone who wants a different one.
318
-
319
- ## API
320
-
321
- | Export | From | What it is |
322
- |---|---|---|
323
- | `createEventSources` | `hood-alerts/events` | Every source for a mainnet client. Throws on testnet, where neither launchpad exists |
324
- | `createNoxaLaunchSource`, `createOdysseyLaunchSource`, `createOdysseyCurveTradeSource`, `createOdysseyGraduationSource`, `createWhaleTradeSource` | `hood-alerts/events` | The sources individually |
325
- | `createPriceOracle`, `createStaticPriceOracle` | `hood-alerts/events` | USD valuation from live liquidity, or a fixed rate |
326
- | `createLiquidityReader` | `hood-alerts/events` | Pool reserves in USD, cached |
327
- | `createMemoryPoolRegistry` | `hood-alerts/events` | The tracked memecoin pool set |
328
- | `fetchLogRange` | `hood-alerts/events` | `eth_getLogs` that bisects on the result cap and retries rate limits |
329
- | `parseRule`, `safeParseRule`, `ruleSchema`, `subscriptionSchema` | `hood-alerts/rules` | Rule validation |
330
- | `evaluateRule`, `matchRules` | `hood-alerts/rules` | The engine |
331
- | `checkRateLimit`, `createMemoryRateLimitStore` | `hood-alerts/rules` | Per-rule rate limiting |
332
- | `createRpcReputationProvider` | `hood-alerts/rules` | On-chain deployer history |
333
- | `renderAlert` | `hood-alerts/notifiers` | One event rendered for both platforms |
334
- | `createTelegramNotifier`, `createTelegramClient` | `hood-alerts/notifiers` | Telegram Bot API |
335
- | `createDiscordWebhookNotifier`, `createDiscordBotNotifier` | `hood-alerts/notifiers` | Discord, both delivery paths |
336
- | `createCaptureNotifier` | `hood-alerts/notifiers` | Records instead of sending. Backs `DRY_RUN=1` |
337
- | `escapeMarkdownV2`, `escapeHtml`, `escapeDiscordMarkdown` | `hood-alerts/notifiers` | The escapers, individually testable |
338
- | `createCommandRouter`, `COMMANDS` | `hood-alerts/bot` | The shared command surface |
339
- | `createTelegramBot` | `hood-alerts/bot` | Long-polling front end |
340
- | `createDiscordInteractionHandler`, `registerDiscordCommands`, `verifyDiscordSignature` | `hood-alerts/bot` | Slash commands and Ed25519 verification |
341
- | `TIER_POLICIES`, `createEntitlementGate` | `hood-alerts/tiers` | The policy table and the one chokepoint |
342
- | `createStaticEntitlementProvider`, `createUsdgEntitlementProvider` | `hood-alerts/tiers` | Who is premium |
343
- | `AlertStore` | `hood-alerts/service` | SQLite state: subscriptions, outbox, cursors, pools, links |
344
- | `createDispatcher` | `hood-alerts/service` | `pollOnce` and `flushOnce` |
345
- | `createService`, `loadConfig` | `hood-alerts/service` | The whole thing, wired |
346
-
347
- ## Limits and caveats
348
-
349
- - **Mainnet only.** NOXA and The Odyssey are deployed on chain 4663. Building sources against testnet throws rather than reporting an empty chain as "no launches".
350
- - **Whale trades cover a tracked pool set, not 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, so "watch everything" is not something the endpoint can serve. The pool set is assembled from launchpad activity (NOXA pools at launch, Odyssey pools at graduation), capped by `WHALE_POOL_LIMIT` newest-first, with every subscriber's watchlist tokens pinned on top of the cap. A pool the service has never seen a launch for is not watched.
351
- - **A fresh database starts `INITIAL_LOOKBACK_BLOCKS` behind the head.** Deleting the SQLite file loses subscriptions and replays that window. Mount it on a volume.
352
- - **Liquidity is total pool reserves, not tradeable depth.** For concentrated Uniswap v3 positions the reserve inside the active tick can be far smaller. Read `minLiquidityUsd` as "how much is in there", not "how much I can sell into".
353
- - **The rug figure is a heuristic**, defined precisely above. It is evidence, not a verdict.
354
- - **Rate-limit quota is consumed at enqueue, not at send.** An alert that is queued and then dead-lettered still counted against the hour. Counting at send time would let one block's events blow through every cap before the first delivery.
355
- - **The public RPC throttles bursts.** Above a few hundred tracked pools, set `ROBINHOOD_RPC_URL` to a dedicated endpoint.
356
-
357
- ## Development
299
+ ## Notes
358
300
 
359
- ```bash
360
- npm install
361
- npm run typecheck
362
- npm test # 219 tests
363
- npm run build
364
- npm run verify:chain # against the real RPC, no credentials
365
- ```
301
+ - **Stock Tokens.** Stock Tokens are tokenized debt securities (issuer: Robinhood Assets (Jersey)
302
+ Ltd) and may not be offered, sold, or delivered to US persons (additional limits: Canada, UK,
303
+ Switzerland). hood-alerts only ever displays price/premium data derived from public Chainlink
304
+ feeds and DEX pools; it never facilitates acquiring a Stock Token.
305
+ - Liquidity-pull alerts are an early warning, not proof of a rug: always check the pool before
306
+ acting on one.
307
+ - Not affiliated with Robinhood Markets, Inc.
366
308
 
367
309
  ## License
368
310
 
369
- Proprietary, all rights reserved. See [LICENSE](./LICENSE).
311
+ MIT. See [LICENSE](./LICENSE).
312
+
313
+ Built by [nirholas](https://x.com/nichxbt) · [three.ws](https://three.ws)