hood-alerts 0.1.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 (107) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +369 -0
  3. package/dist/bot/index.cjs +1008 -0
  4. package/dist/bot/index.cjs.map +1 -0
  5. package/dist/bot/index.d.cts +241 -0
  6. package/dist/bot/index.d.ts +241 -0
  7. package/dist/bot/index.js +30 -0
  8. package/dist/bot/index.js.map +1 -0
  9. package/dist/chunk-32GS6XVE.js +732 -0
  10. package/dist/chunk-32GS6XVE.js.map +1 -0
  11. package/dist/chunk-3LHCQA4Z.js +76 -0
  12. package/dist/chunk-3LHCQA4Z.js.map +1 -0
  13. package/dist/chunk-3NHLSKZE.js +1 -0
  14. package/dist/chunk-3NHLSKZE.js.map +1 -0
  15. package/dist/chunk-6JQK5G3Z.js +72 -0
  16. package/dist/chunk-6JQK5G3Z.js.map +1 -0
  17. package/dist/chunk-CBNZ6AZT.js +598 -0
  18. package/dist/chunk-CBNZ6AZT.js.map +1 -0
  19. package/dist/chunk-CIURPPOW.js +1 -0
  20. package/dist/chunk-CIURPPOW.js.map +1 -0
  21. package/dist/chunk-IUOEGBF6.js +49 -0
  22. package/dist/chunk-IUOEGBF6.js.map +1 -0
  23. package/dist/chunk-NBMINJ5E.js +230 -0
  24. package/dist/chunk-NBMINJ5E.js.map +1 -0
  25. package/dist/chunk-O2RY2SWN.js +1 -0
  26. package/dist/chunk-O2RY2SWN.js.map +1 -0
  27. package/dist/chunk-PLMVKTQM.js +1 -0
  28. package/dist/chunk-PLMVKTQM.js.map +1 -0
  29. package/dist/chunk-RSTLW7XH.js +130 -0
  30. package/dist/chunk-RSTLW7XH.js.map +1 -0
  31. package/dist/chunk-SOBZ2FPP.js +355 -0
  32. package/dist/chunk-SOBZ2FPP.js.map +1 -0
  33. package/dist/chunk-VC5MKEY2.js +170 -0
  34. package/dist/chunk-VC5MKEY2.js.map +1 -0
  35. package/dist/chunk-W2GUAWUR.js +147 -0
  36. package/dist/chunk-W2GUAWUR.js.map +1 -0
  37. package/dist/chunk-Y2M3USWZ.js +355 -0
  38. package/dist/chunk-Y2M3USWZ.js.map +1 -0
  39. package/dist/chunk-Z4DJPESQ.js +1216 -0
  40. package/dist/chunk-Z4DJPESQ.js.map +1 -0
  41. package/dist/chunk-ZDM5VNWB.js +1 -0
  42. package/dist/chunk-ZDM5VNWB.js.map +1 -0
  43. package/dist/events/index.cjs +820 -0
  44. package/dist/events/index.cjs.map +1 -0
  45. package/dist/events/index.d.cts +58 -0
  46. package/dist/events/index.d.ts +58 -0
  47. package/dist/events/index.js +69 -0
  48. package/dist/events/index.js.map +1 -0
  49. package/dist/format-6RG4OcyE.d.ts +57 -0
  50. package/dist/format-B7d40f65.d.cts +57 -0
  51. package/dist/index.cjs +4122 -0
  52. package/dist/index.cjs.map +1 -0
  53. package/dist/index.d.cts +21 -0
  54. package/dist/index.d.ts +21 -0
  55. package/dist/index.js +274 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/logger-3phvV_fl.d.cts +185 -0
  58. package/dist/logger-CYza_0eU.d.ts +185 -0
  59. package/dist/logs-D7sYBvSy.d.cts +66 -0
  60. package/dist/logs-D7sYBvSy.d.ts +66 -0
  61. package/dist/notifiers/index.cjs +620 -0
  62. package/dist/notifiers/index.cjs.map +1 -0
  63. package/dist/notifiers/index.d.cts +186 -0
  64. package/dist/notifiers/index.d.ts +186 -0
  65. package/dist/notifiers/index.js +70 -0
  66. package/dist/notifiers/index.js.map +1 -0
  67. package/dist/pricing-BumgLpvB.d.cts +79 -0
  68. package/dist/pricing-BumgLpvB.d.ts +79 -0
  69. package/dist/ratelimit-BKVQG2oY.d.cts +42 -0
  70. package/dist/ratelimit-CGLlXNIS.d.ts +42 -0
  71. package/dist/registry-D3We8Lg6.d.ts +50 -0
  72. package/dist/registry-DePA3IyS.d.cts +50 -0
  73. package/dist/rules/index.cjs +614 -0
  74. package/dist/rules/index.cjs.map +1 -0
  75. package/dist/rules/index.d.cts +166 -0
  76. package/dist/rules/index.d.ts +166 -0
  77. package/dist/rules/index.js +60 -0
  78. package/dist/rules/index.js.map +1 -0
  79. package/dist/schema-DuDMPqpq.d.ts +183 -0
  80. package/dist/schema-NnIdKFl2.d.cts +183 -0
  81. package/dist/service/index.cjs +3774 -0
  82. package/dist/service/index.cjs.map +1 -0
  83. package/dist/service/index.d.cts +262 -0
  84. package/dist/service/index.d.ts +262 -0
  85. package/dist/service/index.js +45 -0
  86. package/dist/service/index.js.map +1 -0
  87. package/dist/service/main.cjs +3759 -0
  88. package/dist/service/main.cjs.map +1 -0
  89. package/dist/service/main.d.cts +1 -0
  90. package/dist/service/main.d.ts +1 -0
  91. package/dist/service/main.js +60 -0
  92. package/dist/service/main.js.map +1 -0
  93. package/dist/sources-CAaQR6-N.d.ts +159 -0
  94. package/dist/sources-CpDOUlzx.d.cts +159 -0
  95. package/dist/telegram-CpodYJrP.d.cts +64 -0
  96. package/dist/telegram-pvphmBwn.d.ts +64 -0
  97. package/dist/tiers/index.cjs +471 -0
  98. package/dist/tiers/index.cjs.map +1 -0
  99. package/dist/tiers/index.d.cts +231 -0
  100. package/dist/tiers/index.d.ts +231 -0
  101. package/dist/tiers/index.js +34 -0
  102. package/dist/tiers/index.js.map +1 -0
  103. package/dist/types-C0ETog04.d.ts +109 -0
  104. package/dist/types-CcoKpStA.d.cts +109 -0
  105. package/dist/types-DFPpTMWo.d.cts +186 -0
  106. package/dist/types-DFPpTMWo.d.ts +186 -0
  107. package/package.json +114 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/events/types.ts","../src/events/explorer.ts","../src/events/pricing.ts","../src/events/tokens.ts","../src/events/blocktime.ts","../src/events/registry.ts","../src/events/liquidity.ts","../src/events/logs.ts","../src/events/sources.ts","../src/rules/schema.ts","../src/rules/reputation.ts","../src/rules/engine.ts","../src/rules/ratelimit.ts","../src/notifiers/escape.ts","../src/notifiers/format.ts","../src/notifiers/http.ts","../src/notifiers/types.ts","../src/notifiers/telegram.ts","../src/notifiers/discord.ts","../src/notifiers/capture.ts","../src/bot/commands.ts","../src/tiers/entitlements.ts","../src/service/logger.ts","../src/bot/telegram-bot.ts","../src/bot/discord-bot.ts","../src/tiers/policy.ts","../src/tiers/enforce.ts","../src/service/config.ts","../src/service/store.ts","../src/service/codec.ts","../src/service/dispatcher.ts","../src/service/http.ts","../src/service/runner.ts"],"sourcesContent":["/**\n * hood-alerts: Telegram and Discord alert bots for Robinhood Chain memecoins\n * (chain ID 4663).\n *\n * Chain watching is `hoodchain` and `hoodkit`. This package is everything\n * above them: a normalized event taxonomy over both launchpads, a\n * schema-validated rule engine, rate-limit-correct delivery adapters, the bot\n * command surface, an enforced tier policy, and a restart-safe hosted service.\n *\n * @packageDocumentation\n */\n\nexport * from './events/index.js'\nexport * from './rules/index.js'\nexport * from './notifiers/index.js'\nexport * from './bot/index.js'\nexport * from './tiers/index.js'\nexport * from './service/index.js'\n","import type { Address, Hash } from 'viem'\n\n/**\n * The normalized alert event taxonomy for Robinhood Chain memecoins.\n *\n * Two launchpads run on mainnet (chain 4663) and they have genuinely\n * different lifecycles. The taxonomy models that difference instead of\n * pretending they are the same product:\n *\n * - **NOXA** (`fun.noxa.fi/robinhood`) is an *instant* launcher. One\n * transaction deploys the ERC-20, creates a Uniswap v3 pool, seeds\n * single-sided liquidity and locks the LP NFT. There is no bonding curve,\n * so NOXA emits `launch` and (through its pool) `whale_trade`, and it can\n * never emit `curve_trade` or `graduation`.\n * - **The Odyssey** (`theodyssey.fun`) is a pump.fun-style native-ETH bonding\n * curve with virtual reserves. It emits `launch` when a curve opens,\n * `curve_trade` for every buy/sell on the curve, and `graduation` when the\n * curve fills and liquidity migrates to a locked Uniswap v3 pool. After\n * graduation its tokens also produce `whale_trade` from the migrated pool.\n *\n * A `graduation` alert therefore covers The Odyssey only. Claiming NOXA\n * \"graduations\" would be describing an event that does not exist on chain.\n */\n\n/** Which launchpad an event originated from. */\nexport type Launchpad = 'noxa' | 'odyssey'\n\n/** The kinds of alert this package emits. */\nexport type AlertEventKind = 'launch' | 'curve_trade' | 'graduation' | 'whale_trade'\n\n/** Buy or sell, from the trader's perspective. */\nexport type TradeSide = 'buy' | 'sell'\n\n/** Explorer deep links attached to every event. */\nexport interface ExplorerLinks {\n /** The transaction that produced the event. */\n tx: string\n /** The token contract. */\n token: string\n /** The actor (deployer or trader). */\n actor: string\n /** The Uniswap v3 pool, when the event has one. */\n pool?: string\n}\n\n/** Fields every alert event carries. */\nexport interface AlertEventBase {\n /**\n * Stable, content-addressed identity for this event: `kind:txHash:logIndex`.\n * Reprocessing the same block range regenerates the identical id, which is\n * what makes the delivery dedupe table work across restarts and reorg\n * re-reads.\n */\n id: string\n kind: AlertEventKind\n launchpad: Launchpad\n /** The memecoin the event is about. */\n token: Address\n /** Token symbol read from the ERC-20, or `null` when the call reverts. */\n symbol: string | null\n /** Token name read from the ERC-20, or `null` when the call reverts. */\n name: string | null\n /** The wallet responsible: the deployer for launches, the trader for trades. */\n actor: Address\n blockNumber: bigint\n logIndex: number\n transactionHash: Hash\n /**\n * Block timestamp in milliseconds since epoch, or `null` when the event was\n * decoded without a block fetch (the source fills it whenever available).\n */\n timestampMs: number | null\n /**\n * USD value of the event, or `null` when it cannot be derived from a real\n * price source. Never guessed: launches carry the seeded/initial-buy value,\n * trades carry the traded notional, and `null` means \"no USDG or WETH leg to\n * price against\", which rules treat as \"does not satisfy a min-USD filter\".\n */\n usdValue: number | null\n explorer: ExplorerLinks\n}\n\n/**\n * A new token launch.\n *\n * NOXA: decoded from `TokenLaunched` on the launch factory. `pool` is present\n * immediately and `initialBuyAmount` is the deployer's own first buy.\n * Odyssey: decoded from `TokenCreated` on a bonding-curve factory. `pool` is\n * `null` because the curve holds the liquidity until graduation.\n */\nexport interface LaunchEvent extends AlertEventBase {\n kind: 'launch'\n /** The Uniswap v3 pool, or `null` for an Odyssey token still on its curve. */\n pool: Address | null\n /** The quote token of the pool (NOXA only, `null` on Odyssey). */\n pairToken: Address | null\n /** Raw initial buy in the pair token's units (NOXA), else `0n`. */\n initialBuyAmount: bigint\n /**\n * The Uniswap v3 LP position NFT minted for this launch (NOXA), or `null`\n * when the launchpad did not mint one at launch time (every Odyssey token,\n * whose LP appears at graduation). Reputation rules read its owner to prove\n * the LP is locked.\n */\n positionId: bigint | null\n /**\n * `true` when this launchpad lists instantly with no bonding curve, so no\n * `graduation` event will ever follow. Always `true` for NOXA. For Odyssey\n * it is `true` only for launches from the instant factory variant.\n */\n instantListing: boolean\n}\n\n/**\n * A bonding-curve buy or sell. The Odyssey only: NOXA has no curve.\n * Decoded from `Traded` on an Odyssey factory.\n */\nexport interface CurveTradeEvent extends AlertEventBase {\n kind: 'curve_trade'\n launchpad: 'odyssey'\n side: TradeSide\n /** Token amount bought or sold, raw (18 decimals). */\n tokenAmount: bigint\n /** Native ETH paid or received, in wei. */\n quoteAmountWei: bigint\n /** Protocol fee taken on this trade, in wei. */\n feeWei: bigint\n /** Curve virtual quote reserve after the trade, in wei. */\n virtualQuoteWei: bigint\n /** Curve virtual token reserve after the trade, raw. */\n virtualTokenAmount: bigint\n /** Price of one token in ETH implied by this fill, or `null` for a zero-size fill. */\n priceEth: number | null\n}\n\n/**\n * A curve that filled and migrated to a locked Uniswap v3 pool.\n * The Odyssey only. Decoded from `PoolMigrated`.\n */\nexport interface GraduationEvent extends AlertEventBase {\n kind: 'graduation'\n launchpad: 'odyssey'\n /** The Uniswap v3 pool liquidity migrated into. */\n pool: Address\n /** The position manager token id holding the migrated liquidity. */\n positionId: bigint\n /** Uniswap v3 liquidity units minted at migration. */\n liquidity: bigint\n /** Token side used to seed the pool, raw. */\n tokenUsed: bigint\n /** Quote side used to seed the pool, raw (6 decimals when the quote is USDG). */\n quoteUsed: bigint\n}\n\n/**\n * A Uniswap v3 swap on a memecoin pool above the configured USD threshold.\n * Both launchpads produce these: NOXA from block one, Odyssey after\n * graduation.\n */\nexport interface WhaleTradeEvent extends AlertEventBase {\n kind: 'whale_trade'\n pool: Address\n /** The pool's quote token (the non-memecoin side). */\n quoteToken: Address\n /** Quote token symbol, e.g. `WETH` or `USDG`. */\n quoteSymbol: string\n side: TradeSide\n /** Memecoin amount that moved, in human units. */\n tokenAmount: number\n /** Quote amount that moved, in human units. */\n quoteAmount: number\n /** Executed price of the memecoin in quote-token units. */\n price: number\n /** Uniswap v3 fee tier of the pool, in hundredths of a bip (3000 = 0.3%). */\n feeTier: number\n}\n\n/** Any normalized alert event. */\nexport type AlertEvent = LaunchEvent | CurveTradeEvent | GraduationEvent | WhaleTradeEvent\n\n/** Narrow an event to a kind. */\nexport function isKind<K extends AlertEventKind>(\n event: AlertEvent,\n kind: K,\n): event is Extract<AlertEvent, { kind: K }> {\n return event.kind === kind\n}\n\n/**\n * Human-readable one-line label for an event kind, used by the bot and the\n * notifier formatters.\n */\nexport const EVENT_KIND_LABELS: Record<AlertEventKind, string> = {\n launch: 'New launch',\n curve_trade: 'Curve trade',\n graduation: 'Graduation',\n whale_trade: 'Whale trade',\n}\n\n/**\n * Which event kinds each launchpad can actually produce. The rule engine uses\n * this to reject impossible rules (for example: Odyssey-only `graduation`\n * combined with a NOXA-only launchpad filter would never fire).\n */\nexport const LAUNCHPAD_EVENT_KINDS: Record<Launchpad, readonly AlertEventKind[]> = {\n noxa: ['launch', 'whale_trade'],\n odyssey: ['launch', 'curve_trade', 'graduation', 'whale_trade'],\n}\n","import type { Address, Hash } from 'viem'\n\n/** Blockscout explorer base URL for Robinhood Chain mainnet (chain 4663). */\nexport const MAINNET_EXPLORER = 'https://robinhoodchain.blockscout.com'\n\n/** Explorer base URL for Robinhood Chain testnet (chain 46630). */\nexport const TESTNET_EXPLORER = 'https://explorer.testnet.chain.robinhood.com'\n\n/** Resolve the explorer base URL for a network name. */\nexport function explorerBase(network: 'mainnet' | 'testnet'): string {\n return network === 'testnet' ? TESTNET_EXPLORER : MAINNET_EXPLORER\n}\n\n/** Link to a transaction. */\nexport function txUrl(base: string, hash: Hash): string {\n return `${base}/tx/${hash}`\n}\n\n/** Link to an address (wallet or contract). */\nexport function addressUrl(base: string, address: Address): string {\n return `${base}/address/${address}`\n}\n\n/** Link to a token page (Blockscout renders holders/transfers there). */\nexport function tokenUrl(base: string, token: Address): string {\n return `${base}/token/${token}`\n}\n\n/** Link to a block. */\nexport function blockUrl(base: string, blockNumber: bigint): string {\n return `${base}/block/${blockNumber}`\n}\n","import { formatUnits, type Address } from 'viem'\nimport {\n MAINNET_ADDRESSES,\n TESTNET_ADDRESSES,\n USDG_DECIMALS,\n quoteSwap,\n type HoodClient,\n} from 'hoodchain'\n\n/**\n * USD valuation for alert events.\n *\n * There is no hardcoded price anywhere in this package. Every dollar figure\n * comes from live chain state:\n *\n * - **USDG legs are the unit of account.** USDG is Paxos' Global Dollar, a\n * fully reserved 1:1 USD stablecoin with 6 decimals. A USDG leg of a trade\n * is its own USD value, no oracle needed.\n * - **ETH legs are priced through the chain's own Uniswap v3 liquidity.** The\n * oracle asks `hoodchain`'s `quoteSwap` for the real output of selling 1\n * WETH into USDG across every fee tier and two-hop route, and takes the best\n * route's output as the ETH/USD rate. That is the price a trader would\n * actually get, sourced from the same pools the alerts watch.\n * - **Anything else is `null`.** A memecoin/memecoin pool with no USDG or WETH\n * leg has no honest USD value from on-chain data alone, so the event carries\n * `usdValue: null` and min-USD rules simply do not match it.\n *\n * Rates are cached for a short TTL so a burst of events in one block does not\n * re-quote the router once per event.\n */\n\n/** A source of USD rates for the two priceable legs. */\nexport interface PriceOracle {\n /**\n * USD per 1 ETH, or `null` when no WETH/USDG route has liquidity right now.\n * Never throws: a quote failure degrades to `null`, which downgrades USD\n * filters rather than dropping the alert pipeline.\n */\n ethUsd(): Promise<number | null>\n /** Convert a raw wei amount to USD, or `null` when the rate is unavailable. */\n weiToUsd(wei: bigint): Promise<number | null>\n /** Convert a raw USDG amount (6 decimals) to USD. Always available. */\n usdgToUsd(atomic: bigint): number\n}\n\n/** Options for {@link createPriceOracle}. */\nexport interface PriceOracleOptions {\n /**\n * How long a quoted ETH/USD rate stays fresh, in milliseconds.\n * @defaultValue `30_000`\n */\n ttlMs?: number\n /**\n * Probe size used to quote WETH into USDG. Larger sizes are less sensitive\n * to a thin tick but need deeper liquidity to fill.\n * @defaultValue `1` ETH\n */\n probeWei?: bigint\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\n/** WETH and USDG for the client's network. */\nexport function quoteTokens(client: HoodClient): { weth: Address; usdg: Address } {\n return client.network === 'testnet'\n ? { weth: TESTNET_ADDRESSES.weth, usdg: TESTNET_ADDRESSES.usdg }\n : { weth: MAINNET_ADDRESSES.weth, usdg: MAINNET_ADDRESSES.usdg }\n}\n\n/**\n * Build a caching ETH/USD oracle backed by real Uniswap v3 quotes.\n *\n * @example\n * ```ts\n * import { createHoodClient } from 'hoodchain'\n * import { createPriceOracle } from 'hood-alerts/events'\n *\n * const hood = createHoodClient()\n * const oracle = createPriceOracle(hood)\n * console.log('ETH/USD from chain liquidity:', await oracle.ethUsd())\n * ```\n */\nexport function createPriceOracle(client: HoodClient, options: PriceOracleOptions = {}): PriceOracle {\n const ttlMs = options.ttlMs ?? 30_000\n const probeWei = options.probeWei ?? 10n ** 18n\n const now = options.now ?? Date.now\n const { weth, usdg } = quoteTokens(client)\n\n let cached: number | null = null\n let cachedAt = 0\n let inflight: Promise<number | null> | null = null\n\n async function refresh(): Promise<number | null> {\n try {\n const quote = await quoteSwap(client, { tokenIn: weth, tokenOut: usdg, amountIn: probeWei })\n const usdgOut = Number(formatUnits(quote.amountOut, USDG_DECIMALS))\n const ethIn = Number(formatUnits(probeWei, 18))\n const rate = ethIn > 0 ? usdgOut / ethIn : 0\n cached = rate > 0 ? rate : null\n } catch {\n // No route or no liquidity right now. Report \"unknown\", never a guess.\n cached = null\n }\n cachedAt = now()\n return cached\n }\n\n async function ethUsd(): Promise<number | null> {\n if (cachedAt !== 0 && now() - cachedAt < ttlMs) return cached\n // Coalesce concurrent refreshes so a block full of events makes one quote.\n inflight ??= refresh().finally(() => {\n inflight = null\n })\n return inflight\n }\n\n return {\n ethUsd,\n async weiToUsd(wei: bigint): Promise<number | null> {\n const rate = await ethUsd()\n if (rate === null) return null\n return Number(formatUnits(wei < 0n ? -wei : wei, 18)) * rate\n },\n usdgToUsd(atomic: bigint): number {\n return Number(formatUnits(atomic < 0n ? -atomic : atomic, USDG_DECIMALS))\n },\n }\n}\n\n/**\n * A fixed-rate oracle. Useful when an operator already has a trusted ETH/USD\n * feed and wants alerts to agree with it, and for deterministic tests.\n */\nexport function createStaticPriceOracle(ethUsdRate: number | null): PriceOracle {\n return {\n async ethUsd() {\n return ethUsdRate\n },\n async weiToUsd(wei: bigint) {\n if (ethUsdRate === null) return null\n return Number(formatUnits(wei < 0n ? -wei : wei, 18)) * ethUsdRate\n },\n usdgToUsd(atomic: bigint) {\n return Number(formatUnits(atomic < 0n ? -atomic : atomic, USDG_DECIMALS))\n },\n }\n}\n","import { getAddress, type Address } from 'viem'\nimport { erc20Abi, type HoodClient } from 'hoodchain'\n\n/** ERC-20 display metadata, with `null` for anything the contract does not answer. */\nexport interface TokenMeta {\n address: Address\n symbol: string | null\n name: string | null\n /** Decimals, defaulting to 18 when the call reverts (the launchpad standard). */\n decimals: number\n}\n\n/** Cached reader for ERC-20 display metadata. */\nexport interface TokenMetaReader {\n get(token: Address): Promise<TokenMeta>\n /** Prime the cache without a network round trip (used by the service store). */\n prime(meta: TokenMeta): void\n}\n\n/**\n * Build a token metadata reader.\n *\n * Metadata is immutable in practice for launchpad tokens, so entries are\n * cached for the process lifetime. A token whose `symbol()`/`name()` revert\n * (some minimal proxies omit them) resolves to `null` fields rather than\n * throwing, because a missing symbol must never drop an alert.\n */\nexport function createTokenMetaReader(client: HoodClient, maxEntries = 5_000): TokenMetaReader {\n const cache = new Map<string, TokenMeta>()\n const inflight = new Map<string, Promise<TokenMeta>>()\n\n function remember(meta: TokenMeta): void {\n if (cache.size >= maxEntries) {\n const oldest = cache.keys().next()\n if (!oldest.done) cache.delete(oldest.value)\n }\n cache.set(meta.address.toLowerCase(), meta)\n }\n\n async function load(token: Address): Promise<TokenMeta> {\n const results = await client.public.multicall({\n contracts: [\n { address: token, abi: erc20Abi, functionName: 'symbol' },\n { address: token, abi: erc20Abi, functionName: 'name' },\n { address: token, abi: erc20Abi, functionName: 'decimals' },\n ],\n allowFailure: true,\n })\n const [symbolResult, nameResult, decimalsResult] = results\n const meta: TokenMeta = {\n address: getAddress(token),\n symbol:\n symbolResult && symbolResult.status === 'success' ? String(symbolResult.result) : null,\n name: nameResult && nameResult.status === 'success' ? String(nameResult.result) : null,\n decimals:\n decimalsResult && decimalsResult.status === 'success' ? Number(decimalsResult.result) : 18,\n }\n remember(meta)\n return meta\n }\n\n return {\n async get(token: Address): Promise<TokenMeta> {\n const key = token.toLowerCase()\n const hit = cache.get(key)\n if (hit) return hit\n const pending = inflight.get(key)\n if (pending) return pending\n const promise = load(token)\n .catch((): TokenMeta => {\n // A dead RPC must not break event construction: fall back to a\n // metadata-free record. The address is always enough to alert on.\n const fallback: TokenMeta = { address: getAddress(token), symbol: null, name: null, decimals: 18 }\n remember(fallback)\n return fallback\n })\n .finally(() => {\n inflight.delete(key)\n })\n inflight.set(key, promise)\n return promise\n },\n prime(meta: TokenMeta): void {\n remember(meta)\n },\n }\n}\n","import type { HoodClient } from 'hoodchain'\n\n/** Cached block-timestamp reader. */\nexport interface BlockTimeReader {\n /**\n * Timestamp of `blockNumber` in ms, or `null` when it could not be read\n * within the caller's budget. Events carry `timestampMs: null` in that case\n * rather than a fabricated \"now\".\n */\n get(blockNumber: bigint): Promise<number | null>\n /**\n * Resolve timestamps for a set of blocks, bounded by `maxLookups`. Blocks\n * past the budget resolve to `null`: a backfill of a million blocks must not\n * turn into a million `eth_getBlockByNumber` calls.\n */\n getMany(blocks: readonly bigint[], maxLookups: number): Promise<Map<bigint, number | null>>\n}\n\n/** Build a block-timestamp reader with a bounded LRU cache. */\nexport function createBlockTimeReader(client: HoodClient, maxEntries = 2_000): BlockTimeReader {\n const cache = new Map<string, number>()\n\n function remember(block: bigint, ts: number): void {\n if (cache.size >= maxEntries) {\n const oldest = cache.keys().next()\n if (!oldest.done) cache.delete(oldest.value)\n }\n cache.set(block.toString(), ts)\n }\n\n async function get(blockNumber: bigint): Promise<number | null> {\n const hit = cache.get(blockNumber.toString())\n if (hit !== undefined) return hit\n try {\n const block = await client.public.getBlock({ blockNumber, includeTransactions: false })\n const ms = Number(block.timestamp) * 1000\n remember(blockNumber, ms)\n return ms\n } catch {\n return null\n }\n }\n\n return {\n get,\n async getMany(blocks: readonly bigint[], maxLookups: number): Promise<Map<bigint, number | null>> {\n const unique = [...new Set(blocks.map((b) => b.toString()))].map((b) => BigInt(b))\n const out = new Map<bigint, number | null>()\n let budget = maxLookups\n for (const block of unique) {\n const cached = cache.get(block.toString())\n if (cached !== undefined) {\n out.set(block, cached)\n continue\n }\n if (budget <= 0) {\n out.set(block, null)\n continue\n }\n budget -= 1\n out.set(block, await get(block))\n }\n return out\n },\n }\n}\n","import { getAddress, type Address } from 'viem'\nimport type { Launchpad } from './types.js'\n\n/**\n * A memecoin pool the whale watcher tracks.\n *\n * Why a registry exists at all: chain-wide `Swap` queries are not viable on\n * Robinhood Chain. A topic-only query for the Uniswap v3 `Swap` signature\n * overflows the RPC's 10,000-log result cap in well under 2,000 blocks (the\n * chain produces a block roughly every 100-130ms and is busy). The whale\n * source therefore watches an explicit, address-filtered *active pool set*\n * assembled from launchpad activity: NOXA pools are known at launch, Odyssey\n * pools are known at graduation.\n */\nexport interface PoolEntry {\n pool: Address\n /** The memecoin side of the pool. */\n token: Address\n /** The quote side (WETH or USDG). */\n quoteToken: Address\n launchpad: Launchpad\n /** Block the pool became known (launch for NOXA, graduation for Odyssey). */\n createdBlock: bigint\n}\n\n/** Storage for known memecoin pools and token origins. */\nexport interface PoolRegistry {\n /** Record (or update) a pool. Idempotent. */\n record(entry: PoolEntry): Promise<void>\n /**\n * The pools to watch for whale trades, newest first, capped at `limit`.\n * `pinned` tokens (subscriber watchlists) are always included regardless of\n * age, because a watchlist alert that silently ages out would be a lie.\n */\n active(limit: number, pinned?: readonly Address[]): Promise<PoolEntry[]>\n /** Which launchpad a token came from, or `null` if never seen. */\n originOf(token: Address): Promise<Launchpad | null>\n /** Every pool recorded for a token. */\n poolsFor(token: Address): Promise<PoolEntry[]>\n /** Total pools recorded, for `/metrics`. */\n size(): Promise<number>\n}\n\n/**\n * In-memory pool registry. Correct and dependency-free, but it starts empty\n * on every process start, so the service uses the SQLite-backed registry from\n * `hood-alerts/service` instead. This one is the right choice for scripts,\n * examples and tests.\n */\nexport function createMemoryPoolRegistry(entries: readonly PoolEntry[] = []): PoolRegistry {\n const pools = new Map<string, PoolEntry>()\n for (const entry of entries) pools.set(entry.pool.toLowerCase(), entry)\n\n return {\n async record(entry: PoolEntry): Promise<void> {\n pools.set(entry.pool.toLowerCase(), {\n ...entry,\n pool: getAddress(entry.pool),\n token: getAddress(entry.token),\n quoteToken: getAddress(entry.quoteToken),\n })\n },\n async active(limit: number, pinned: readonly Address[] = []): Promise<PoolEntry[]> {\n const pinnedSet = new Set(pinned.map((a) => a.toLowerCase()))\n const all = [...pools.values()].sort((a, b) => (a.createdBlock > b.createdBlock ? -1 : 1))\n const isPinned = (entry: PoolEntry): boolean => pinnedSet.has(entry.token.toLowerCase())\n // Pinned pools are additional to the limit, not counted against it: a\n // watchlist alert that aged out of the window would be a silent lie.\n const picked = all.filter(isPinned)\n let recent = 0\n for (const entry of all) {\n if (recent >= limit) break\n if (isPinned(entry)) continue\n picked.push(entry)\n recent += 1\n }\n return picked\n },\n async originOf(token: Address): Promise<Launchpad | null> {\n const key = token.toLowerCase()\n for (const entry of pools.values()) {\n if (entry.token.toLowerCase() === key) return entry.launchpad\n }\n return null\n },\n async poolsFor(token: Address): Promise<PoolEntry[]> {\n const key = token.toLowerCase()\n return [...pools.values()].filter((entry) => entry.token.toLowerCase() === key)\n },\n async size(): Promise<number> {\n return pools.size\n },\n }\n}\n","import { formatUnits, type Address } from 'viem'\nimport { USDG_DECIMALS, erc20Abi, type HoodClient } from 'hoodchain'\nimport type { PriceOracle } from './pricing.js'\nimport { quoteTokens } from './pricing.js'\nimport type { PoolRegistry } from './registry.js'\n\n/**\n * Pool liquidity in USD, measured the only way that is honest without an\n * indexer: the pool's live balance of its quote token, doubled.\n *\n * A Uniswap v3 pool holds real balances of both sides. The quote side (WETH\n * or USDG) is the side with a known USD value, so `quoteBalance * 2` is the\n * standard two-sided TVL approximation and it is exactly what a trader means\n * by \"how much liquidity is in there\". For concentrated positions this is the\n * *total* reserve rather than the reserve inside the active tick, which is\n * documented rather than papered over: min/max liquidity rules filter on this\n * number, so subscribers should read it as pool reserves, not tradeable depth.\n */\nexport interface LiquidityReader {\n /** USD liquidity of a specific pool, or `null` when the quote side is unpriceable. */\n poolLiquidityUsd(pool: Address, quoteToken: Address): Promise<number | null>\n /**\n * Best-known USD liquidity for a token: the largest of its recorded pools.\n * `null` when the token has no recorded pool (an Odyssey token still on its\n * bonding curve, for example).\n */\n tokenLiquidityUsd(token: Address): Promise<number | null>\n}\n\n/** Options for {@link createLiquidityReader}. */\nexport interface LiquidityReaderOptions {\n /** Cache lifetime for a pool's USD liquidity, in ms. @defaultValue `60_000` */\n ttlMs?: number\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\ninterface CacheEntry {\n value: number | null\n at: number\n}\n\n/** Build a caching liquidity reader over live pool balances. */\nexport function createLiquidityReader(\n client: HoodClient,\n oracle: PriceOracle,\n registry: PoolRegistry,\n options: LiquidityReaderOptions = {},\n): LiquidityReader {\n const ttlMs = options.ttlMs ?? 60_000\n const now = options.now ?? Date.now\n const cache = new Map<string, CacheEntry>()\n const { weth, usdg } = quoteTokens(client)\n\n async function readPool(pool: Address, quoteToken: Address): Promise<number | null> {\n const quote = quoteToken.toLowerCase()\n if (quote !== weth.toLowerCase() && quote !== usdg.toLowerCase()) return null\n const balance = await client.public.readContract({\n address: quoteToken,\n abi: erc20Abi,\n functionName: 'balanceOf',\n args: [pool],\n })\n if (quote === usdg.toLowerCase()) {\n return Number(formatUnits(balance, USDG_DECIMALS)) * 2\n }\n const usd = await oracle.weiToUsd(balance)\n return usd === null ? null : usd * 2\n }\n\n async function poolLiquidityUsd(pool: Address, quoteToken: Address): Promise<number | null> {\n const key = pool.toLowerCase()\n const hit = cache.get(key)\n if (hit && now() - hit.at < ttlMs) return hit.value\n let value: number | null\n try {\n value = await readPool(pool, quoteToken)\n } catch {\n // A failed read must not be reported as zero liquidity: that would\n // silently satisfy every `maxLiquidityUsd` rule. Unknown stays unknown.\n value = null\n }\n cache.set(key, { value, at: now() })\n return value\n }\n\n return {\n poolLiquidityUsd,\n async tokenLiquidityUsd(token: Address): Promise<number | null> {\n const pools = await registry.poolsFor(token)\n if (pools.length === 0) return null\n const values = await Promise.all(\n pools.map((entry) => poolLiquidityUsd(entry.pool, entry.quoteToken)),\n )\n const known = values.filter((v): v is number => v !== null)\n return known.length === 0 ? null : Math.max(...known)\n },\n }\n}\n","import type { Address } from 'viem'\n\n/**\n * Resilient log fetching for Robinhood Chain's public RPC.\n *\n * Two behaviours of the public endpoint drive this helper, both measured\n * against `https://rpc.mainnet.chain.robinhood.com` while building this\n * package:\n *\n * 1. **There is no block-range cap, but there is a 10,000-log result cap.**\n * A query spanning millions of blocks succeeds if it is selective enough\n * (the three Odyssey factories return every log they ever emitted in one\n * call); a query spanning 2,000 blocks fails with\n * `logs matched by query exceeds limit of 10000` if it is not (a topic-only\n * Uniswap v3 `Swap` query does). Fixed-size range chunking therefore\n * cannot guarantee success, so {@link fetchLogRange} bisects the range on\n * that specific error until each half fits.\n * 2. **It rate limits bursts with `Too Many Requests`.** Those are retried\n * with exponential backoff rather than surfaced, because a retryable 429\n * must never advance a caller's block cursor.\n */\n\n/** Options for {@link fetchLogRange}. */\nexport interface FetchLogRangeOptions {\n /**\n * Smallest range the bisector will produce. A single block that still\n * overflows the result cap throws, because there is nothing left to split.\n * @defaultValue `1n`\n */\n minSpan?: bigint\n /** Retries for rate-limit and transient transport errors. @defaultValue `5` */\n retries?: number\n /** Base backoff in ms, doubled per attempt. @defaultValue `500` */\n backoffMs?: number\n /** Sleep injection point for tests. @defaultValue a real timer */\n sleep?: (ms: number) => Promise<void>\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/** `true` when the RPC rejected the query for returning too many logs. */\nexport function isResultTooLarge(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return /exceeds limit|too many results|response size exceeded|query returned more than/i.test(message)\n}\n\n/** `true` when the error is a transient rate limit or timeout worth retrying. */\nexport function isRetryableRpcError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return /too many requests|rate limit|timed out|timeout|429|503|502|ECONNRESET|socket hang up|fetch failed/i.test(\n message,\n )\n}\n\n/**\n * Fetch logs for `[from, to]` inclusive through `fetcher`, bisecting the range\n * when the RPC reports too many results and retrying transient failures.\n *\n * @throws the underlying error when a single block still overflows the result\n * cap, or when retries are exhausted. Callers must treat a throw as \"range not\n * processed\" and leave their cursor untouched.\n *\n * @example\n * ```ts\n * const logs = await fetchLogRange(\n * (from, to) => hood.public.getLogs({ address: factory, event, fromBlock: from, toBlock: to }),\n * 0n,\n * await hood.public.getBlockNumber(),\n * )\n * ```\n */\nexport async function fetchLogRange<T>(\n fetcher: (from: bigint, to: bigint) => Promise<readonly T[]>,\n from: bigint,\n to: bigint,\n options: FetchLogRangeOptions = {},\n): Promise<T[]> {\n const minSpan = options.minSpan ?? 1n\n const retries = options.retries ?? 5\n const backoffMs = options.backoffMs ?? 500\n const sleep = options.sleep ?? defaultSleep\n\n const run = async (lo: bigint, hi: bigint): Promise<T[]> => {\n let attempt = 0\n for (;;) {\n try {\n return [...(await fetcher(lo, hi))]\n } catch (error) {\n if (isResultTooLarge(error)) {\n const span = hi - lo\n if (span < minSpan || span === 0n) throw error\n const mid = lo + span / 2n\n const left = await run(lo, mid)\n const right = await run(mid + 1n, hi)\n return [...left, ...right]\n }\n if (isRetryableRpcError(error) && attempt < retries) {\n await sleep(backoffMs * 2 ** attempt)\n attempt += 1\n continue\n }\n throw error\n }\n }\n }\n\n if (to < from) return []\n return run(from, to)\n}\n\n/**\n * Split a list of addresses into query-sized batches. The RPC accepts long\n * address arrays, but batching keeps any single response under the result cap\n * and keeps one failing batch from invalidating the whole set.\n */\nexport function batchAddresses(addresses: readonly Address[], size = 200): Address[][] {\n const batches: Address[][] = []\n for (let i = 0; i < addresses.length; i += size) {\n batches.push(addresses.slice(i, i + size) as Address[])\n }\n return batches\n}\n","import { getAddress, type Address, type Hash } from 'viem'\nimport {\n NOXA_ADDRESSES,\n ODYSSEY_ADDRESSES,\n noxaTokenLaunchedEvent,\n odysseyTokenCreatedEvent,\n odysseyTradedEvent,\n odysseyPoolMigratedEvent,\n type HoodClient,\n} from 'hoodchain'\nimport { loadPoolInfo, uniswapV3SwapEvent, type PoolInfo } from 'hoodkit'\nimport { addressUrl, explorerBase, tokenUrl, txUrl } from './explorer.js'\nimport { batchAddresses, fetchLogRange, type FetchLogRangeOptions } from './logs.js'\nimport { createBlockTimeReader, type BlockTimeReader } from './blocktime.js'\nimport { quoteTokens, type PriceOracle } from './pricing.js'\nimport type { PoolRegistry } from './registry.js'\nimport type { TokenMetaReader } from './tokens.js'\nimport type {\n AlertEvent,\n CurveTradeEvent,\n ExplorerLinks,\n GraduationEvent,\n LaunchEvent,\n Launchpad,\n WhaleTradeEvent,\n} from './types.js'\n\n/**\n * The event sources: one per on-chain surface, each responsible for turning a\n * confirmed block range into normalized {@link AlertEvent}s.\n *\n * Sources are deliberately *pull* based (`poll(from, to)`) rather than\n * push/subscription based. The service owns the block cursor and persists it,\n * so a source that is asked for the same range twice must produce identical\n * events, and it does: every event id is derived from `txHash` and `logIndex`.\n * That is what makes crash recovery safe.\n */\n\n/** A pollable on-chain source of alert events. */\nexport interface EventSource {\n /** Stable identifier, used as the persisted cursor key. Never change it. */\n id: string\n /** Human label for logs and `/metrics`. */\n label: string\n /** Earliest block this source can produce events from. */\n startBlock: bigint\n /** Decode `[from, to]` inclusive into events. Must be deterministic. */\n poll(from: bigint, to: bigint): Promise<AlertEvent[]>\n}\n\n/** Wiring shared by every source. */\nexport interface SourceContext {\n client: HoodClient\n oracle: PriceOracle\n tokens: TokenMetaReader\n registry: PoolRegistry\n /** Optional shared block-timestamp reader (one is created when omitted). */\n blockTimes?: BlockTimeReader\n /** Max `eth_getBlockByNumber` calls per poll, for timestamps. @defaultValue `25` */\n maxBlockTimeLookupsPerPoll?: number\n /** Retry/bisect options passed to {@link fetchLogRange}. */\n logOptions?: FetchLogRangeOptions\n /**\n * Tokens that must always be in the whale watcher's pool set (subscriber\n * watchlists). Called once per whale poll.\n */\n pinnedTokens?: () => Promise<readonly Address[]>\n /**\n * Max pools the whale watcher queries per poll, newest first.\n * @defaultValue `400`\n */\n whalePoolLimit?: number\n /**\n * Service-wide floor for whale trades in USD. Swaps below this never become\n * events at all, which keeps the delivery pipeline from doing per-subscriber\n * work on dust. Per-subscription `minUsd` rules are applied on top and can\n * only be stricter.\n * @defaultValue `1000`\n */\n whaleMinUsd?: number\n /** Pools per address-filtered `eth_getLogs` call. @defaultValue `200` */\n whalePoolBatchSize?: number\n}\n\nconst ODYSSEY_FACTORIES: Address[] = [\n ODYSSEY_ADDRESSES.bondingCurveFactory,\n ODYSSEY_ADDRESSES.reflectionFactory,\n ODYSSEY_ADDRESSES.instantFactory,\n]\n\n/** Deterministic event id: reprocessing a range regenerates the same value. */\nexport function eventId(kind: string, transactionHash: Hash, logIndex: number): string {\n return `${kind}:${transactionHash.toLowerCase()}:${logIndex}`\n}\n\nfunction links(base: string, token: Address, actor: Address, hash: Hash, pool?: Address): ExplorerLinks {\n return {\n tx: txUrl(base, hash),\n token: tokenUrl(base, token),\n actor: addressUrl(base, actor),\n ...(pool ? { pool: addressUrl(base, pool) } : {}),\n }\n}\n\n/**\n * Value a raw amount of a quote token in USD. Returns `null` for any token\n * that is neither USDG nor WETH, because there is no honest rate for it.\n */\nasync function valueQuote(\n ctx: SourceContext,\n quoteToken: Address,\n raw: bigint,\n): Promise<number | null> {\n const { weth, usdg } = quoteTokens(ctx.client)\n const token = quoteToken.toLowerCase()\n if (token === usdg.toLowerCase()) return ctx.oracle.usdgToUsd(raw)\n if (token === weth.toLowerCase()) return ctx.oracle.weiToUsd(raw)\n return null\n}\n\nfunction assertMainnet(client: HoodClient): void {\n if (client.network !== 'mainnet') {\n throw new Error(\n 'hood-alerts: the NOXA and Odyssey launchpads are deployed on Robinhood Chain mainnet (chain 4663) only. ' +\n 'Create the client with { chain: \"mainnet\" } (the default) to watch them.',\n )\n }\n}\n\n/**\n * Build every event source for a mainnet client.\n *\n * @throws when the client targets testnet: neither launchpad exists there, so\n * silently returning an empty source list would look like \"no launches\".\n *\n * @example\n * ```ts\n * import { createHoodClient } from 'hoodchain'\n * import {\n * createEventSources, createPriceOracle, createTokenMetaReader, createMemoryPoolRegistry,\n * } from 'hood-alerts/events'\n *\n * const hood = createHoodClient()\n * const sources = createEventSources({\n * client: hood,\n * oracle: createPriceOracle(hood),\n * tokens: createTokenMetaReader(hood),\n * registry: createMemoryPoolRegistry(),\n * })\n * const head = await hood.public.getBlockNumber()\n * for (const source of sources) {\n * console.log(source.label, (await source.poll(head - 2_000n, head)).length)\n * }\n * ```\n */\nexport function createEventSources(ctx: SourceContext): EventSource[] {\n assertMainnet(ctx.client)\n return [\n createNoxaLaunchSource(ctx),\n createOdysseyLaunchSource(ctx),\n createOdysseyCurveTradeSource(ctx),\n createOdysseyGraduationSource(ctx),\n createWhaleTradeSource(ctx),\n ]\n}\n\nfunction blockTimes(ctx: SourceContext): BlockTimeReader {\n ctx.blockTimes ??= createBlockTimeReader(ctx.client)\n return ctx.blockTimes\n}\n\nasync function stampTimes(\n ctx: SourceContext,\n events: AlertEvent[],\n): Promise<AlertEvent[]> {\n if (events.length === 0) return events\n const reader = blockTimes(ctx)\n const times = await reader.getMany(\n events.map((e) => e.blockNumber),\n ctx.maxBlockTimeLookupsPerPoll ?? 25,\n )\n for (const event of events) event.timestampMs = times.get(event.blockNumber) ?? null\n return events\n}\n\n/** NOXA `TokenLaunched`: an instant listing with a live Uniswap v3 pool. */\nexport function createNoxaLaunchSource(ctx: SourceContext): EventSource {\n const base = explorerBase(ctx.client.network)\n return {\n id: 'noxa:launch',\n label: 'NOXA launches',\n startBlock: NOXA_ADDRESSES.deployBlock,\n async poll(from: bigint, to: bigint): Promise<AlertEvent[]> {\n const logs = await fetchLogRange(\n (lo, hi) =>\n ctx.client.public.getLogs({\n address: NOXA_ADDRESSES.launchFactory,\n event: noxaTokenLaunchedEvent,\n fromBlock: lo,\n toBlock: hi,\n }),\n from,\n to,\n ctx.logOptions ?? {},\n )\n\n const events: LaunchEvent[] = []\n for (const log of logs) {\n const token = log.args.token as Address | undefined\n const deployer = log.args.deployer as Address | undefined\n if (!token || !deployer) continue\n const pool = (log.args.pool as Address | undefined) ?? null\n const pairToken = (log.args.pairToken as Address | undefined) ?? null\n const initialBuyAmount = (log.args.initialBuyAmount as bigint | undefined) ?? 0n\n\n if (pool && pairToken) {\n await ctx.registry.record({\n pool: getAddress(pool),\n token: getAddress(token),\n quoteToken: getAddress(pairToken),\n launchpad: 'noxa',\n createdBlock: log.blockNumber,\n })\n }\n const meta = await ctx.tokens.get(token)\n const usdValue =\n pairToken && initialBuyAmount > 0n ? await valueQuote(ctx, pairToken, initialBuyAmount) : null\n\n events.push({\n id: eventId('launch', log.transactionHash, log.logIndex),\n kind: 'launch',\n launchpad: 'noxa',\n token: getAddress(token),\n symbol: meta.symbol,\n name: meta.name,\n actor: getAddress(deployer),\n blockNumber: log.blockNumber,\n logIndex: log.logIndex,\n transactionHash: log.transactionHash,\n timestampMs: null,\n usdValue,\n explorer: links(base, token, deployer, log.transactionHash, pool ?? undefined),\n pool: pool ? getAddress(pool) : null,\n pairToken: pairToken ? getAddress(pairToken) : null,\n initialBuyAmount,\n positionId: (log.args.positionId as bigint | undefined) ?? null,\n // NOXA has no bonding curve at all: a launch is already a listing.\n instantListing: true,\n })\n }\n return stampTimes(ctx, events)\n },\n }\n}\n\n/** The Odyssey `TokenCreated`: a bonding curve opening. */\nexport function createOdysseyLaunchSource(ctx: SourceContext): EventSource {\n const base = explorerBase(ctx.client.network)\n return {\n id: 'odyssey:launch',\n label: 'Odyssey launches',\n startBlock: NOXA_ADDRESSES.deployBlock,\n async poll(from: bigint, to: bigint): Promise<AlertEvent[]> {\n const logs = await fetchLogRange(\n (lo, hi) =>\n ctx.client.public.getLogs({\n address: ODYSSEY_FACTORIES,\n event: odysseyTokenCreatedEvent,\n fromBlock: lo,\n toBlock: hi,\n }),\n from,\n to,\n ctx.logOptions ?? {},\n )\n\n const events: LaunchEvent[] = []\n for (const log of logs) {\n const token = log.args.token as Address | undefined\n const creator = log.args.creator as Address | undefined\n if (!token || !creator) continue\n const meta = await ctx.tokens.get(token)\n events.push({\n id: eventId('launch', log.transactionHash, log.logIndex),\n kind: 'launch',\n launchpad: 'odyssey',\n token: getAddress(token),\n symbol: meta.symbol,\n name: meta.name,\n actor: getAddress(creator),\n blockNumber: log.blockNumber,\n logIndex: log.logIndex,\n transactionHash: log.transactionHash,\n timestampMs: null,\n // The curve holds liquidity until graduation, so a new Odyssey token\n // has no pool and no priceable notional at creation time.\n usdValue: null,\n explorer: links(base, token, creator, log.transactionHash),\n pool: null,\n pairToken: null,\n initialBuyAmount: 0n,\n positionId: null,\n instantListing:\n log.address.toLowerCase() === ODYSSEY_ADDRESSES.instantFactory.toLowerCase(),\n })\n }\n return stampTimes(ctx, events)\n },\n }\n}\n\n/** The Odyssey `Traded`: every bonding-curve buy and sell. */\nexport function createOdysseyCurveTradeSource(ctx: SourceContext): EventSource {\n const base = explorerBase(ctx.client.network)\n return {\n id: 'odyssey:curve_trade',\n label: 'Odyssey curve trades',\n startBlock: NOXA_ADDRESSES.deployBlock,\n async poll(from: bigint, to: bigint): Promise<AlertEvent[]> {\n const logs = await fetchLogRange(\n (lo, hi) =>\n ctx.client.public.getLogs({\n address: ODYSSEY_FACTORIES,\n event: odysseyTradedEvent,\n fromBlock: lo,\n toBlock: hi,\n }),\n from,\n to,\n ctx.logOptions ?? {},\n )\n\n const events: CurveTradeEvent[] = []\n for (const log of logs) {\n const token = log.args.token as Address | undefined\n const trader = log.args.trader as Address | undefined\n if (!token || !trader) continue\n const tokenAmount = (log.args.tokenAmount as bigint | undefined) ?? 0n\n const quoteAmountWei = (log.args.quoteAmount as bigint | undefined) ?? 0n\n const meta = await ctx.tokens.get(token)\n // The curve quotes in native ETH, so the ETH/USD rate from live\n // WETH/USDG liquidity is the only conversion involved.\n const usdValue = await ctx.oracle.weiToUsd(quoteAmountWei)\n const priceEth =\n tokenAmount > 0n\n ? Number(quoteAmountWei) / Number(tokenAmount)\n : null\n\n events.push({\n id: eventId('curve_trade', log.transactionHash, log.logIndex),\n kind: 'curve_trade',\n launchpad: 'odyssey',\n token: getAddress(token),\n symbol: meta.symbol,\n name: meta.name,\n actor: getAddress(trader),\n blockNumber: log.blockNumber,\n logIndex: log.logIndex,\n transactionHash: log.transactionHash,\n timestampMs: null,\n usdValue,\n explorer: links(base, token, trader, log.transactionHash),\n side: (log.args.isBuy as boolean | undefined) ? 'buy' : 'sell',\n tokenAmount,\n quoteAmountWei,\n feeWei: (log.args.fee as bigint | undefined) ?? 0n,\n virtualQuoteWei: (log.args.virtualQuote as bigint | undefined) ?? 0n,\n virtualTokenAmount: (log.args.virtualToken as bigint | undefined) ?? 0n,\n priceEth,\n })\n }\n return stampTimes(ctx, events)\n },\n }\n}\n\n/**\n * The Odyssey `PoolMigrated`: the curve filled and liquidity moved to a locked\n * Uniswap v3 pool. This is the only real graduation event on the chain. NOXA\n * cannot emit one, by construction.\n */\nexport function createOdysseyGraduationSource(ctx: SourceContext): EventSource {\n const base = explorerBase(ctx.client.network)\n return {\n id: 'odyssey:graduation',\n label: 'Odyssey graduations',\n startBlock: NOXA_ADDRESSES.deployBlock,\n async poll(from: bigint, to: bigint): Promise<AlertEvent[]> {\n const logs = await fetchLogRange(\n (lo, hi) =>\n ctx.client.public.getLogs({\n address: ODYSSEY_FACTORIES,\n event: odysseyPoolMigratedEvent,\n fromBlock: lo,\n toBlock: hi,\n }),\n from,\n to,\n ctx.logOptions ?? {},\n )\n\n const events: GraduationEvent[] = []\n for (const log of logs) {\n const token = log.args.token as Address | undefined\n const pool = log.args.pool as Address | undefined\n if (!token || !pool) continue\n const quoteUsed = (log.args.usdcUsed as bigint | undefined) ?? 0n\n\n // Read the pool to learn which side is the quote token, then record it\n // so the whale watcher starts covering this graduate immediately.\n let quoteToken: Address | null = null\n try {\n const info = await loadPoolInfo(ctx.client, pool)\n quoteToken =\n info.token0.toLowerCase() === token.toLowerCase() ? info.token1 : info.token0\n await ctx.registry.record({\n pool: getAddress(pool),\n token: getAddress(token),\n quoteToken,\n launchpad: 'odyssey',\n createdBlock: log.blockNumber,\n })\n } catch {\n // Pool metadata unavailable (pruned archive state, RPC blip). The\n // graduation is still real and still worth alerting on.\n quoteToken = null\n }\n\n const meta = await ctx.tokens.get(token)\n const usdValue = quoteToken ? await valueQuote(ctx, quoteToken, quoteUsed) : null\n\n events.push({\n id: eventId('graduation', log.transactionHash, log.logIndex),\n kind: 'graduation',\n launchpad: 'odyssey',\n token: getAddress(token),\n symbol: meta.symbol,\n name: meta.name,\n // A migration is executed by the factory on behalf of the curve, so\n // the closest thing to an actor is the factory that emitted it.\n actor: getAddress(log.address),\n blockNumber: log.blockNumber,\n logIndex: log.logIndex,\n transactionHash: log.transactionHash,\n timestampMs: null,\n usdValue,\n explorer: links(base, token, log.address as Address, log.transactionHash, pool),\n pool: getAddress(pool),\n positionId: (log.args.tokenId as bigint | undefined) ?? 0n,\n liquidity: (log.args.liquidity as bigint | undefined) ?? 0n,\n tokenUsed: (log.args.tokenUsed as bigint | undefined) ?? 0n,\n quoteUsed,\n })\n }\n return stampTimes(ctx, events)\n },\n }\n}\n\n/**\n * Uniswap v3 swaps on known memecoin pools, above the service-wide USD floor.\n *\n * The pool set comes from {@link PoolRegistry}: NOXA pools are known from\n * launch, Odyssey pools from graduation. That is a deliberate design choice,\n * not a shortcut. A topic-only `Swap` query across the whole chain overflows\n * the RPC's 10,000-log result cap inside 2,000 blocks, so \"watch every pool\"\n * is not something the public endpoint can serve. Watching the launchpad pool\n * set is both cheaper and exactly the product: memecoin whale alerts.\n */\nexport function createWhaleTradeSource(ctx: SourceContext): EventSource {\n const base = explorerBase(ctx.client.network)\n const poolInfoCache = new Map<string, PoolInfo>()\n const minUsd = ctx.whaleMinUsd ?? 1000\n\n async function poolInfo(pool: Address): Promise<PoolInfo | null> {\n const key = pool.toLowerCase()\n const hit = poolInfoCache.get(key)\n if (hit) return hit\n try {\n const info = await loadPoolInfo(ctx.client, pool)\n poolInfoCache.set(key, info)\n return info\n } catch {\n return null\n }\n }\n\n return {\n id: 'uniswap:whale_trade',\n label: 'Whale trades',\n startBlock: NOXA_ADDRESSES.deployBlock,\n async poll(from: bigint, to: bigint): Promise<AlertEvent[]> {\n const pinned = ctx.pinnedTokens ? await ctx.pinnedTokens() : []\n const entries = await ctx.registry.active(ctx.whalePoolLimit ?? 400, pinned)\n if (entries.length === 0) return []\n\n const byPool = new Map(entries.map((entry) => [entry.pool.toLowerCase(), entry]))\n const batches = batchAddresses(\n entries.map((entry) => entry.pool),\n ctx.whalePoolBatchSize ?? 200,\n )\n\n const logs = (\n await Promise.all(\n batches.map((batch) =>\n fetchLogRange(\n (lo, hi) =>\n ctx.client.public.getLogs({\n address: batch,\n event: uniswapV3SwapEvent,\n fromBlock: lo,\n toBlock: hi,\n }),\n from,\n to,\n ctx.logOptions ?? {},\n ),\n ),\n )\n ).flat()\n\n const events: WhaleTradeEvent[] = []\n for (const log of logs) {\n const entry = byPool.get(log.address.toLowerCase())\n if (!entry) continue\n const info = await poolInfo(entry.pool)\n if (!info) continue\n\n const amount0 = (log.args.amount0 as bigint | undefined) ?? 0n\n const amount1 = (log.args.amount1 as bigint | undefined) ?? 0n\n const quoteIsToken0 = info.token0.toLowerCase() === entry.quoteToken.toLowerCase()\n const quoteRaw = quoteIsToken0 ? amount0 : amount1\n const tokenRaw = quoteIsToken0 ? amount1 : amount0\n if (quoteRaw === 0n || tokenRaw === 0n) continue\n\n const usdValue = await valueQuote(ctx, entry.quoteToken, quoteRaw)\n if (usdValue === null || usdValue < minUsd) continue\n\n const quoteDecimals = quoteIsToken0 ? info.decimals0 : info.decimals1\n const tokenDecimals = quoteIsToken0 ? info.decimals1 : info.decimals0\n const quoteAmount = Math.abs(Number(quoteRaw)) / 10 ** quoteDecimals\n const tokenAmount = Math.abs(Number(tokenRaw)) / 10 ** tokenDecimals\n\n const [tokenMeta, quoteMeta] = await Promise.all([\n ctx.tokens.get(entry.token),\n ctx.tokens.get(entry.quoteToken),\n ])\n const trader = (log.args.recipient as Address | undefined) ?? (log.args.sender as Address)\n\n events.push({\n id: eventId('whale_trade', log.transactionHash, log.logIndex),\n kind: 'whale_trade',\n launchpad: entry.launchpad,\n token: entry.token,\n symbol: tokenMeta.symbol,\n name: tokenMeta.name,\n actor: getAddress(trader),\n blockNumber: log.blockNumber,\n logIndex: log.logIndex,\n transactionHash: log.transactionHash,\n timestampMs: null,\n usdValue,\n explorer: links(base, entry.token, trader, log.transactionHash, entry.pool),\n pool: entry.pool,\n quoteToken: entry.quoteToken,\n quoteSymbol: quoteMeta.symbol ?? 'quote',\n // The memecoin left the pool (negative delta) means the trader bought it.\n side: tokenRaw < 0n ? 'buy' : 'sell',\n tokenAmount,\n quoteAmount,\n price: tokenAmount > 0 ? quoteAmount / tokenAmount : 0,\n feeTier: info.fee,\n })\n }\n return stampTimes(ctx, events)\n },\n }\n}\n\n/** Every launchpad an event source can attribute an event to. */\nexport const SUPPORTED_LAUNCHPADS: readonly Launchpad[] = ['noxa', 'odyssey']\n","import { z } from 'zod'\nimport { LAUNCHPAD_EVENT_KINDS, type AlertEventKind, type Launchpad } from '../events/types.js'\n\n/**\n * Rules are data, not code.\n *\n * Every filter a subscriber can set is expressed as a JSON document validated\n * by these schemas, which means a rule can be stored in SQLite, edited from a\n * Telegram or Discord command, diffed, exported and re-imported without any\n * of it going through `eval`, a DSL parser or a hand-rolled validator. The\n * schema is the single source of truth for what a valid rule is: the bot, the\n * service and the tier policy all validate through it.\n */\n\nconst addressPattern = /^0x[0-9a-fA-F]{40}$/\n\n/** An EVM address, stored lowercase so comparisons never depend on checksum casing. */\nexport const addressSchema = z\n .string()\n .regex(addressPattern, 'must be a 0x-prefixed 20-byte address')\n .transform((value) => value.toLowerCase())\n\n/** The four alert kinds. */\nexport const eventKindSchema = z.enum(['launch', 'curve_trade', 'graduation', 'whale_trade'])\n\n/** The two launchpads. */\nexport const launchpadSchema = z.enum(['noxa', 'odyssey'])\n\n/** Trade direction filter. `any` disables the filter. */\nexport const sideSchema = z.enum(['buy', 'sell', 'any'])\n\n/**\n * Deployer reputation filters, all derived from on-chain history (see\n * `hood-alerts/rules` `createRpcReputationProvider`). Every bound is inclusive.\n */\nexport const reputationFilterSchema = z\n .object({\n /** Require the deployer to have launched at least this many tokens before. */\n minPriorLaunches: z.number().int().min(0).max(10_000).optional(),\n /** Reject serial deployers above this prior-launch count. */\n maxPriorLaunches: z.number().int().min(0).max(10_000).optional(),\n /**\n * Reject a deployer with more than this many prior launches whose pool\n * liquidity has since been drained below the rug threshold.\n */\n maxRuggedLaunches: z.number().int().min(0).max(10_000).optional(),\n /**\n * Require this launch's own LP position to be held by the launchpad's\n * locker contract. Only meaningful for `launch` events that carry a pool\n * (NOXA); events without a pool never satisfy it.\n */\n requireLpLocked: z.boolean().optional(),\n })\n .strict()\n .refine(\n (value) =>\n value.minPriorLaunches === undefined ||\n value.maxPriorLaunches === undefined ||\n value.minPriorLaunches <= value.maxPriorLaunches,\n { message: 'minPriorLaunches must not exceed maxPriorLaunches' },\n )\n\n/** Per-rule delivery rate limits. */\nexport const rateLimitSchema = z\n .object({\n /** Hard cap on alerts this rule may deliver per rolling hour. */\n maxPerHour: z.number().int().min(1).max(10_000).optional(),\n /** Minimum gap between two alerts from this rule, in seconds. */\n minIntervalSeconds: z.number().int().min(1).max(86_400).optional(),\n })\n .strict()\n\n/** A single alert rule. */\nexport const ruleSchema = z\n .object({\n /** Stable id, unique within a subscription. Used in bot commands. */\n id: z\n .string()\n .min(1)\n .max(32)\n .regex(/^[a-z0-9][a-z0-9_-]*$/, 'must be lowercase letters, digits, hyphen or underscore'),\n /** Human label shown in `/rules`. */\n name: z.string().min(1).max(64).optional(),\n enabled: z.boolean().default(true),\n /** Event kinds this rule reacts to. Defaults to all four. */\n kinds: z.array(eventKindSchema).min(1).max(4).default(['launch', 'curve_trade', 'graduation', 'whale_trade']),\n /** Launchpads this rule reacts to. Defaults to both. */\n launchpads: z.array(launchpadSchema).min(1).max(2).default(['noxa', 'odyssey']),\n /** Minimum USD value of the event. Events with an unknown value never pass. */\n minUsd: z.number().min(0).max(1e12).optional(),\n /** Maximum USD value of the event. */\n maxUsd: z.number().min(0).max(1e12).optional(),\n /** Minimum USD liquidity of the token's deepest known pool. */\n minLiquidityUsd: z.number().min(0).max(1e12).optional(),\n /** Maximum USD liquidity of the token's deepest known pool. */\n maxLiquidityUsd: z.number().min(0).max(1e12).optional(),\n /** Watchlist. When non-empty, only these tokens match. */\n tokens: z.array(addressSchema).max(500).default([]),\n /** Deployer watchlist. When non-empty, only these actors match. */\n deployers: z.array(addressSchema).max(500).default([]),\n /** Deployers to always reject, applied after every other filter. */\n excludeDeployers: z.array(addressSchema).max(500).default([]),\n /** Trade direction. Ignored by `launch` and `graduation` events. */\n side: sideSchema.default('any'),\n reputation: reputationFilterSchema.optional(),\n rateLimit: rateLimitSchema.optional(),\n })\n .strict()\n .refine((rule) => rule.minUsd === undefined || rule.maxUsd === undefined || rule.minUsd <= rule.maxUsd, {\n message: 'minUsd must not exceed maxUsd',\n path: ['minUsd'],\n })\n .refine(\n (rule) =>\n rule.minLiquidityUsd === undefined ||\n rule.maxLiquidityUsd === undefined ||\n rule.minLiquidityUsd <= rule.maxLiquidityUsd,\n { message: 'minLiquidityUsd must not exceed maxLiquidityUsd', path: ['minLiquidityUsd'] },\n )\n .refine((rule) => rule.kinds.some((kind) => rule.launchpads.some((pad) => canEmit(pad, kind))), {\n message:\n 'this combination can never fire: NOXA is an instant launcher with no bonding curve, so it emits no curve_trade and no graduation',\n path: ['kinds'],\n })\n\n/** A validated rule. */\nexport type Rule = z.output<typeof ruleSchema>\n/** A rule as accepted on input, before defaults are applied. */\nexport type RuleInput = z.input<typeof ruleSchema>\n/** Validated reputation filter. */\nexport type ReputationFilter = z.output<typeof reputationFilterSchema>\n/** Validated rate limit block. */\nexport type RateLimitConfig = z.output<typeof rateLimitSchema>\n\n/** Delivery platforms a subscription can target. */\nexport const platformSchema = z.enum(['telegram', 'discord'])\n/** A delivery platform. */\nexport type Platform = z.output<typeof platformSchema>\n\n/** A subscription: one delivery target plus the rules that feed it. */\nexport const subscriptionSchema = z\n .object({\n id: z.string().min(1).max(64),\n /**\n * The account that owns this subscription, and the unit entitlements are\n * checked against. For Telegram this is the user id, for Discord the guild\n * id (a server pays once for its channels) or user id for a DM.\n */\n subscriberId: z.string().min(1).max(128),\n platform: platformSchema,\n /**\n * Where alerts go: a Telegram chat id, a Discord channel id, or a Discord\n * webhook URL. Validated per platform by the notifier, not here, because\n * the same subscription record serves all three shapes.\n */\n target: z.string().min(1).max(512),\n enabled: z.boolean().default(true),\n rules: z.array(ruleSchema).max(100).default([]),\n createdAtMs: z.number().int().nonnegative(),\n })\n .strict()\n .refine((sub) => new Set(sub.rules.map((rule) => rule.id)).size === sub.rules.length, {\n message: 'rule ids must be unique within a subscription',\n path: ['rules'],\n })\n\n/** A validated subscription. */\nexport type Subscription = z.output<typeof subscriptionSchema>\n/** A subscription as accepted on input. */\nexport type SubscriptionInput = z.input<typeof subscriptionSchema>\n\n/** Can a launchpad emit this event kind at all? */\nexport function canEmit(launchpad: Launchpad, kind: AlertEventKind): boolean {\n return LAUNCHPAD_EVENT_KINDS[launchpad].includes(kind)\n}\n\n/**\n * How many filters a rule actually uses. The tier policy caps this so free\n * subscribers get real filtering without turning the service into an unpaid\n * on-chain query engine.\n */\nexport function ruleComplexity(rule: Rule): number {\n let score = 0\n if (rule.minUsd !== undefined) score += 1\n if (rule.maxUsd !== undefined) score += 1\n if (rule.minLiquidityUsd !== undefined) score += 1\n if (rule.maxLiquidityUsd !== undefined) score += 1\n if (rule.tokens.length > 0) score += 1\n if (rule.deployers.length > 0) score += 1\n if (rule.excludeDeployers.length > 0) score += 1\n if (rule.side !== 'any') score += 1\n if (rule.reputation) score += Object.values(rule.reputation).filter((v) => v !== undefined).length\n if (rule.rateLimit) score += 1\n return score\n}\n\n/** Filters that cost an extra on-chain read per candidate event. */\nexport function usesOnChainLookups(rule: Rule): boolean {\n return (\n rule.minLiquidityUsd !== undefined ||\n rule.maxLiquidityUsd !== undefined ||\n (rule.reputation !== undefined &&\n Object.values(rule.reputation).some((value) => value !== undefined))\n )\n}\n\n/**\n * Parse and validate a rule, applying defaults.\n *\n * @throws {@link z.ZodError} with a field-level message the bot renders back\n * to the user verbatim.\n *\n * @example\n * ```ts\n * const rule = parseRule({ id: 'whales', kinds: ['whale_trade'], minUsd: 5000 })\n * console.log(rule.launchpads) // ['noxa', 'odyssey'] (defaulted)\n * ```\n */\nexport function parseRule(input: unknown): Rule {\n return ruleSchema.parse(input)\n}\n\n/** Non-throwing {@link parseRule}. */\nexport function safeParseRule(input: unknown): z.ZodSafeParseResult<Rule> {\n return ruleSchema.safeParse(input)\n}\n\n/** Parse and validate a subscription. */\nexport function parseSubscription(input: unknown): Subscription {\n return subscriptionSchema.parse(input)\n}\n\n/** Non-throwing {@link parseSubscription}. */\nexport function safeParseSubscription(input: unknown): z.ZodSafeParseResult<Subscription> {\n return subscriptionSchema.safeParse(input)\n}\n\n/**\n * Render a Zod error as one line of human-readable text, for bot replies.\n * A user who typed `/threshold abc` gets \"minUsd: expected number\", not a\n * stack trace and not a silent failure.\n */\nexport function formatValidationError(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.join('.')\n return path ? `${path}: ${issue.message}` : issue.message\n })\n .join('; ')\n}\n\n/** A ready-to-use starter rule set, used by `/subscribe` before any editing. */\nexport function defaultRules(): Rule[] {\n return [\n parseRule({ id: 'launches', name: 'New launches', kinds: ['launch'] }),\n parseRule({\n id: 'graduations',\n name: 'Odyssey graduations',\n kinds: ['graduation'],\n launchpads: ['odyssey'],\n }),\n parseRule({ id: 'whales', name: 'Whale trades over $5k', kinds: ['whale_trade'], minUsd: 5000 }),\n ]\n}\n","import { formatUnits, getAddress, type Address } from 'viem'\nimport {\n MAINNET_ADDRESSES,\n NOXA_ADDRESSES,\n ODYSSEY_ADDRESSES,\n USDG_DECIMALS,\n erc20Abi,\n noxaTokenLaunchedEvent,\n odysseyTokenCreatedEvent,\n type HoodClient,\n} from 'hoodchain'\nimport { fetchLogRange, type FetchLogRangeOptions } from '../events/logs.js'\nimport { quoteTokens, type PriceOracle } from '../events/pricing.js'\nimport type { Launchpad } from '../events/types.js'\n\n/**\n * Deployer reputation, derived entirely from chain state.\n *\n * Nothing here is scraped, self-reported or scored by a model. Three\n * measurements, each with an exact on-chain definition:\n *\n * - **Prior launches**: `TokenLaunched` logs on the NOXA factory with\n * `deployer` indexed to this address, plus `TokenCreated` logs on the three\n * Odyssey factories with `creator` indexed to this address. Both queries are\n * topic-selective, so the RPC serves them across the full chain history in\n * one call each.\n * - **LP locked**: for a NOXA launch, the LP position NFT is locked when\n * `NonfungiblePositionManager.ownerOf(positionId)` is the NOXA locker\n * contract. That is the launchpad's own permanent-lock mechanism, read\n * directly rather than trusted from a UI badge.\n * - **Rugged**: a prior launch whose pool quote-token reserve has since fallen\n * below `rugThresholdUsd`. Liquidity that was seeded and is now gone is the\n * observable footprint of a pull. It is a heuristic and is labelled as one\n * everywhere it surfaces: a token that simply never traded and a token whose\n * liquidity was withdrawn both end up with an empty pool, and only the\n * second is a rug. Pair it with `minPriorLaunches` for a meaningful signal.\n *\n * Results are cached per deployer, because a launch burst from one deployer\n * would otherwise re-run the same history scan per event.\n */\n\n/** One prior launch by a deployer. */\nexport interface PriorLaunch {\n launchpad: Launchpad\n token: Address\n pool: Address | null\n quoteToken: Address | null\n /** NOXA LP position id, when the launch had one. */\n positionId: bigint | null\n blockNumber: bigint\n}\n\n/** A deployer's on-chain track record. */\nexport interface DeployerReputation {\n deployer: Address\n /** Every launch found, oldest first. Capped by `maxHistory`. */\n launches: PriorLaunch[]\n /** Launches whose LP position is held by the NOXA locker. */\n lockedLaunches: number\n /** Launches whose pool reserve is now below the rug threshold. */\n ruggedLaunches: number\n /**\n * How many launches were actually inspected for lock/rug status. Lower than\n * `launches.length` when the history exceeds `maxInspect`, and surfaced so\n * consumers never read a partial scan as a clean record.\n */\n inspected: number\n /** When this record was computed, ms since epoch. */\n computedAtMs: number\n}\n\n/** Reputation lookups for the rule engine. */\nexport interface ReputationProvider {\n get(deployer: Address): Promise<DeployerReputation>\n /** Is a specific LP position locked by the launchpad locker? */\n isLpLocked(positionId: bigint): Promise<boolean>\n}\n\n/** Options for {@link createRpcReputationProvider}. */\nexport interface ReputationProviderOptions {\n /** Cache lifetime per deployer, in ms. @defaultValue `600_000` (10 minutes) */\n ttlMs?: number\n /** Max launches kept per deployer. @defaultValue `200` */\n maxHistory?: number\n /**\n * Max prior launches inspected for lock/rug status per deployer. Each one\n * costs a pool balance read, so this bounds the RPC cost of a rule.\n * @defaultValue `25`\n */\n maxInspect?: number\n /** Pool reserve below this USD value counts as drained. @defaultValue `50` */\n rugThresholdUsd?: number\n /** Retry/bisect options for the history queries. */\n logOptions?: FetchLogRangeOptions\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\nconst positionManagerAbi = [\n {\n type: 'function',\n name: 'ownerOf',\n stateMutability: 'view',\n inputs: [{ name: 'tokenId', type: 'uint256' }],\n outputs: [{ type: 'address' }],\n },\n] as const\n\nconst ODYSSEY_FACTORIES: Address[] = [\n ODYSSEY_ADDRESSES.bondingCurveFactory,\n ODYSSEY_ADDRESSES.reflectionFactory,\n ODYSSEY_ADDRESSES.instantFactory,\n]\n\n/**\n * Build an RPC-backed reputation provider.\n *\n * @example\n * ```ts\n * const reputation = createRpcReputationProvider(hood, oracle)\n * const record = await reputation.get('0xDeployer…')\n * console.log(record.launches.length, 'prior launches,', record.ruggedLaunches, 'drained')\n * ```\n */\nexport function createRpcReputationProvider(\n client: HoodClient,\n oracle: PriceOracle,\n options: ReputationProviderOptions = {},\n): ReputationProvider {\n const ttlMs = options.ttlMs ?? 600_000\n const maxHistory = options.maxHistory ?? 200\n const maxInspect = options.maxInspect ?? 25\n const rugThresholdUsd = options.rugThresholdUsd ?? 50\n const now = options.now ?? Date.now\n const cache = new Map<string, DeployerReputation>()\n const inflight = new Map<string, Promise<DeployerReputation>>()\n const lockCache = new Map<string, boolean>()\n const { weth, usdg } = quoteTokens(client)\n\n async function reserveUsd(pool: Address, quoteToken: Address): Promise<number | null> {\n const quote = quoteToken.toLowerCase()\n if (quote !== weth.toLowerCase() && quote !== usdg.toLowerCase()) return null\n const balance = await client.public.readContract({\n address: quoteToken,\n abi: erc20Abi,\n functionName: 'balanceOf',\n args: [pool],\n })\n if (quote === usdg.toLowerCase()) return Number(formatUnits(balance, USDG_DECIMALS))\n return oracle.weiToUsd(balance)\n }\n\n async function isLpLocked(positionId: bigint): Promise<boolean> {\n const key = positionId.toString()\n const hit = lockCache.get(key)\n if (hit !== undefined) return hit\n try {\n const owner = await client.public.readContract({\n address: MAINNET_ADDRESSES.nonfungiblePositionManager,\n abi: positionManagerAbi,\n functionName: 'ownerOf',\n args: [positionId],\n })\n const locked = owner.toLowerCase() === NOXA_ADDRESSES.locker.toLowerCase()\n lockCache.set(key, locked)\n return locked\n } catch {\n // A burned or non-existent position reverts. Unknown is not locked.\n return false\n }\n }\n\n async function load(deployer: Address): Promise<DeployerReputation> {\n const head = await client.public.getBlockNumber()\n const [noxaLogs, odysseyLogs] = await Promise.all([\n fetchLogRange(\n (from, to) =>\n client.public.getLogs({\n address: NOXA_ADDRESSES.launchFactory,\n event: noxaTokenLaunchedEvent,\n args: { deployer },\n fromBlock: from,\n toBlock: to,\n }),\n NOXA_ADDRESSES.deployBlock,\n head,\n options.logOptions ?? {},\n ),\n fetchLogRange(\n (from, to) =>\n client.public.getLogs({\n address: ODYSSEY_FACTORIES,\n event: odysseyTokenCreatedEvent,\n args: { creator: deployer },\n fromBlock: from,\n toBlock: to,\n }),\n NOXA_ADDRESSES.deployBlock,\n head,\n options.logOptions ?? {},\n ),\n ])\n\n const launches: PriorLaunch[] = []\n for (const log of noxaLogs) {\n const token = log.args.token as Address | undefined\n if (!token) continue\n const pool = (log.args.pool as Address | undefined) ?? null\n const pairToken = (log.args.pairToken as Address | undefined) ?? null\n launches.push({\n launchpad: 'noxa',\n token: getAddress(token),\n pool: pool ? getAddress(pool) : null,\n quoteToken: pairToken ? getAddress(pairToken) : null,\n positionId: (log.args.positionId as bigint | undefined) ?? null,\n blockNumber: log.blockNumber,\n })\n }\n for (const log of odysseyLogs) {\n const token = log.args.token as Address | undefined\n if (!token) continue\n launches.push({\n launchpad: 'odyssey',\n token: getAddress(token),\n pool: null,\n quoteToken: null,\n positionId: null,\n blockNumber: log.blockNumber,\n })\n }\n launches.sort((a, b) => (a.blockNumber < b.blockNumber ? -1 : a.blockNumber > b.blockNumber ? 1 : 0))\n const kept = launches.slice(-maxHistory)\n\n // Inspect the most recent launches for lock and drain status.\n const inspectable = kept.slice(-maxInspect)\n let lockedLaunches = 0\n let ruggedLaunches = 0\n for (const launch of inspectable) {\n if (launch.positionId !== null && (await isLpLocked(launch.positionId))) lockedLaunches += 1\n if (launch.pool && launch.quoteToken) {\n try {\n const usd = await reserveUsd(launch.pool, launch.quoteToken)\n if (usd !== null && usd < rugThresholdUsd) ruggedLaunches += 1\n } catch {\n // An unreadable pool is not evidence of a rug. Skip it.\n }\n }\n }\n\n return {\n deployer: getAddress(deployer),\n launches: kept,\n lockedLaunches,\n ruggedLaunches,\n inspected: inspectable.length,\n computedAtMs: now(),\n }\n }\n\n return {\n async get(deployer: Address): Promise<DeployerReputation> {\n const key = deployer.toLowerCase()\n const hit = cache.get(key)\n if (hit && now() - hit.computedAtMs < ttlMs) return hit\n const pending = inflight.get(key)\n if (pending) return pending\n const promise = load(deployer)\n .then((record) => {\n cache.set(key, record)\n return record\n })\n .finally(() => {\n inflight.delete(key)\n })\n inflight.set(key, promise)\n return promise\n },\n isLpLocked,\n }\n}\n\n/**\n * Count a deployer's launches that happened strictly before a block. Rules ask\n * \"how many had they launched *at the time of this event*\", which is not the\n * same as \"how many have they launched now\", and only the first is stable when\n * a historical range is re-processed after a restart.\n */\nexport function priorLaunchesBefore(record: DeployerReputation, blockNumber: bigint): number {\n return record.launches.filter((launch) => launch.blockNumber < blockNumber).length\n}\n","import type { Address } from 'viem'\nimport type { AlertEvent } from '../events/types.js'\nimport { priorLaunchesBefore, type DeployerReputation } from './reputation.js'\nimport type { Rule } from './schema.js'\n\n/**\n * The rule engine: does this event satisfy this rule?\n *\n * Two properties matter more than the filter list itself.\n *\n * **Lazy, ordered evaluation.** Cheap in-memory filters (kind, launchpad,\n * watchlists, USD bounds) run before anything that costs an RPC call\n * (liquidity, reputation). A rule that rejects an event on its kind never\n * triggers a pool balance read. With thousands of subscriptions this is the\n * difference between a service that keeps up with the chain and one that does\n * not.\n *\n * **Unknown is not a pass.** Every USD and liquidity figure can legitimately\n * be `null` (no USDG or WETH leg to price against, an unreadable pool). A\n * filter that asks for a measurement the chain did not give us does not match.\n * The alternative, treating unknown as zero or as infinity, silently satisfies\n * either `minUsd` or `maxLiquidityUsd` and ships alerts that are simply wrong.\n */\n\n/** Lazily-invoked lookups the engine uses only when a rule needs them. */\nexport interface EvaluationContext {\n /** USD liquidity of the event token's deepest known pool. */\n liquidityUsd?: (event: AlertEvent) => Promise<number | null>\n /** On-chain track record for a deployer. */\n reputation?: (deployer: Address) => Promise<DeployerReputation>\n /** Is this LP position held by the launchpad locker? */\n isLpLocked?: (positionId: bigint) => Promise<boolean>\n}\n\n/** The outcome of evaluating one rule against one event. */\nexport interface MatchResult {\n matched: boolean\n /**\n * Why. On a match this names the rule; on a miss it names the first filter\n * that rejected the event. The bot's `/status` and the service's debug log\n * both surface this, so \"why didn't I get an alert\" is answerable.\n */\n reason: string\n}\n\nconst matchOk = (rule: Rule): MatchResult => ({ matched: true, reason: `rule ${rule.id} matched` })\nconst miss = (reason: string): MatchResult => ({ matched: false, reason })\n\nfunction eventSide(event: AlertEvent): 'buy' | 'sell' | null {\n if (event.kind === 'curve_trade' || event.kind === 'whale_trade') return event.side\n return null\n}\n\nfunction eventPositionId(event: AlertEvent): bigint | null {\n if (event.kind === 'launch') return event.positionId\n if (event.kind === 'graduation') return event.positionId\n return null\n}\n\n/**\n * Evaluate a rule against an event.\n *\n * @example\n * ```ts\n * const rule = parseRule({ id: 'whales', kinds: ['whale_trade'], minUsd: 5000 })\n * const result = await evaluateRule(rule, event)\n * if (!result.matched) console.log('skipped:', result.reason)\n * ```\n */\nexport async function evaluateRule(\n rule: Rule,\n event: AlertEvent,\n context: EvaluationContext = {},\n): Promise<MatchResult> {\n if (!rule.enabled) return miss(`rule ${rule.id} is disabled`)\n\n if (!rule.kinds.includes(event.kind)) {\n return miss(`kind ${event.kind} not in [${rule.kinds.join(', ')}]`)\n }\n if (!rule.launchpads.includes(event.launchpad)) {\n return miss(`launchpad ${event.launchpad} not in [${rule.launchpads.join(', ')}]`)\n }\n\n const actor = event.actor.toLowerCase()\n if (rule.excludeDeployers.includes(actor)) {\n return miss(`actor ${event.actor} is on the exclude list`)\n }\n if (rule.tokens.length > 0 && !rule.tokens.includes(event.token.toLowerCase())) {\n return miss(`token ${event.token} is not on the watchlist`)\n }\n if (rule.deployers.length > 0 && !rule.deployers.includes(actor)) {\n return miss(`actor ${event.actor} is not on the deployer watchlist`)\n }\n\n if (rule.side !== 'any') {\n const side = eventSide(event)\n if (side === null) return miss(`side filter does not apply to ${event.kind} events`)\n if (side !== rule.side) return miss(`side ${side} is not ${rule.side}`)\n }\n\n if (rule.minUsd !== undefined) {\n if (event.usdValue === null) return miss('event has no USD value to compare against minUsd')\n if (event.usdValue < rule.minUsd) {\n return miss(`usdValue ${event.usdValue.toFixed(2)} below minUsd ${rule.minUsd}`)\n }\n }\n if (rule.maxUsd !== undefined) {\n if (event.usdValue === null) return miss('event has no USD value to compare against maxUsd')\n if (event.usdValue > rule.maxUsd) {\n return miss(`usdValue ${event.usdValue.toFixed(2)} above maxUsd ${rule.maxUsd}`)\n }\n }\n\n // Everything below costs at least one RPC read.\n if (rule.minLiquidityUsd !== undefined || rule.maxLiquidityUsd !== undefined) {\n if (!context.liquidityUsd) {\n return miss('rule filters on liquidity but no liquidity reader was provided')\n }\n const liquidity = await context.liquidityUsd(event)\n if (liquidity === null) return miss('token liquidity is unknown')\n if (rule.minLiquidityUsd !== undefined && liquidity < rule.minLiquidityUsd) {\n return miss(`liquidity ${liquidity.toFixed(2)} below minLiquidityUsd ${rule.minLiquidityUsd}`)\n }\n if (rule.maxLiquidityUsd !== undefined && liquidity > rule.maxLiquidityUsd) {\n return miss(`liquidity ${liquidity.toFixed(2)} above maxLiquidityUsd ${rule.maxLiquidityUsd}`)\n }\n }\n\n const reputationFilter = rule.reputation\n if (reputationFilter) {\n const needsRecord =\n reputationFilter.minPriorLaunches !== undefined ||\n reputationFilter.maxPriorLaunches !== undefined ||\n reputationFilter.maxRuggedLaunches !== undefined\n\n if (needsRecord) {\n if (!context.reputation) {\n return miss('rule filters on reputation but no reputation provider was configured')\n }\n const record = await context.reputation(event.actor)\n const prior = priorLaunchesBefore(record, event.blockNumber)\n if (\n reputationFilter.minPriorLaunches !== undefined &&\n prior < reputationFilter.minPriorLaunches\n ) {\n return miss(`deployer had ${prior} prior launches, needs ${reputationFilter.minPriorLaunches}`)\n }\n if (\n reputationFilter.maxPriorLaunches !== undefined &&\n prior > reputationFilter.maxPriorLaunches\n ) {\n return miss(`deployer had ${prior} prior launches, max is ${reputationFilter.maxPriorLaunches}`)\n }\n if (\n reputationFilter.maxRuggedLaunches !== undefined &&\n record.ruggedLaunches > reputationFilter.maxRuggedLaunches\n ) {\n return miss(\n `deployer has ${record.ruggedLaunches} drained pools, max is ${reputationFilter.maxRuggedLaunches}`,\n )\n }\n }\n\n if (reputationFilter.requireLpLocked === true) {\n const positionId = eventPositionId(event)\n if (positionId === null) return miss('event carries no LP position to check for a lock')\n if (!context.isLpLocked) {\n return miss('rule requires an LP lock check but no reputation provider was configured')\n }\n if (!(await context.isLpLocked(positionId))) {\n return miss(`LP position ${positionId} is not held by the launchpad locker`)\n }\n }\n }\n\n return matchOk(rule)\n}\n\n/**\n * Evaluate every rule against an event and return the rules that matched,\n * with their reasons. Rules are independent: one throwing lookup does not\n * suppress the others.\n */\nexport async function matchRules(\n rules: readonly Rule[],\n event: AlertEvent,\n context: EvaluationContext = {},\n): Promise<{ rule: Rule; result: MatchResult }[]> {\n const results = await Promise.all(\n rules.map(async (rule) => {\n try {\n return { rule, result: await evaluateRule(rule, event, context) }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return { rule, result: miss(`evaluation failed: ${message}`) }\n }\n }),\n )\n return results.filter((entry) => entry.result.matched)\n}\n","import type { RateLimitConfig } from './schema.js'\n\n/**\n * Rate limiting is a *storage* question, not an in-memory counter question:\n * a service that resets its limits on every deploy is not rate limited. The\n * engine talks to this interface, the service backs it with the same SQLite\n * file that holds the delivery log, and tests back it with a map.\n */\nexport interface RateLimitStore {\n /** How many deliveries were recorded for `key` at or after `sinceMs`. */\n countSince(key: string, sinceMs: number): Promise<number>\n /** When `key` last delivered, or `null` if never. */\n lastAt(key: string): Promise<number | null>\n /**\n * Record a delivery against both its rule bucket and its subscriber. The\n * subscriber attribution is what the tier throughput cap counts, so the two\n * are written together and can never drift apart.\n */\n record(key: string, subscriberId: string, atMs: number): Promise<void>\n /** How many alerts a subscriber received since `sinceMs`, for tier caps. */\n countForSubscriberSince(subscriberId: string, sinceMs: number): Promise<number>\n}\n\n/** Why a rate limit rejected a delivery, or `null` when it allowed it. */\nexport interface RateLimitDecision {\n allowed: boolean\n /** Human-readable reason, safe to log and to show in `/status`. */\n reason: string | null\n /** When the next delivery would be allowed, ms since epoch, if known. */\n retryAtMs: number | null\n}\n\nconst ALLOWED: RateLimitDecision = { allowed: true, reason: null, retryAtMs: null }\n\n/**\n * Check a rule's rate limit without consuming it. Consumption happens through\n * {@link RateLimitStore.record} after a delivery attempt, so a rule that\n * matches but fails entitlement checks does not burn quota.\n */\nexport async function checkRateLimit(\n store: RateLimitStore,\n key: string,\n config: RateLimitConfig | undefined,\n nowMs: number,\n): Promise<RateLimitDecision> {\n if (!config) return ALLOWED\n\n if (config.minIntervalSeconds !== undefined) {\n const last = await store.lastAt(key)\n if (last !== null) {\n const nextAllowed = last + config.minIntervalSeconds * 1000\n if (nowMs < nextAllowed) {\n return {\n allowed: false,\n reason: `minIntervalSeconds=${config.minIntervalSeconds} not elapsed`,\n retryAtMs: nextAllowed,\n }\n }\n }\n }\n\n if (config.maxPerHour !== undefined) {\n const windowStart = nowMs - 3_600_000\n const used = await store.countSince(key, windowStart)\n if (used >= config.maxPerHour) {\n return {\n allowed: false,\n reason: `maxPerHour=${config.maxPerHour} reached (${used} in the last hour)`,\n retryAtMs: windowStart + 3_600_000,\n }\n }\n }\n\n return ALLOWED\n}\n\n/** An in-memory {@link RateLimitStore}. Correct, but resets with the process. */\nexport function createMemoryRateLimitStore(): RateLimitStore {\n const byKey = new Map<string, number[]>()\n const bySubscriber = new Map<string, number[]>()\n\n const push = (map: Map<string, number[]>, id: string, atMs: number): void => {\n const list = map.get(id) ?? []\n list.push(atMs)\n // Keep the window bounded: nothing older than an hour affects a decision.\n map.set(\n id,\n list.filter((at) => at >= atMs - 3_600_000),\n )\n }\n\n return {\n async countSince(key: string, sinceMs: number): Promise<number> {\n return (byKey.get(key) ?? []).filter((at) => at >= sinceMs).length\n },\n async lastAt(key: string): Promise<number | null> {\n const list = byKey.get(key)\n if (!list || list.length === 0) return null\n return list[list.length - 1] ?? null\n },\n async record(key: string, subscriberId: string, atMs: number): Promise<void> {\n push(byKey, key, atMs)\n push(bySubscriber, subscriberId, atMs)\n },\n async countForSubscriberSince(subscriberId: string, sinceMs: number): Promise<number> {\n return (bySubscriber.get(subscriberId) ?? []).filter((at) => at >= sinceMs).length\n },\n }\n}\n\n/** Canonical rate-limit key: one bucket per rule per subscription. */\nexport function rateLimitKey(subscriptionId: string, ruleId: string): string {\n return `${subscriptionId}:${ruleId}`\n}\n","/**\n * Escaping. The single most common way an alert bot breaks in production.\n *\n * Memecoin names are adversarial input by nature. A token called `WHO_LET_THE`\n * silently italicises half a Telegram message under MarkdownV2 and a token\n * called `<b>RUG` injects markup under HTML parse mode; either way Telegram\n * answers `400 Bad Request: can't parse entities` and the alert is lost. The\n * rules are not symmetric between platforms or even between contexts within a\n * platform, so each context gets its own function and its own tests.\n */\n\n/**\n * Telegram MarkdownV2 reserves exactly these characters in normal text.\n * Source: Telegram Bot API \"MarkdownV2 style\" section. All 18 of them must be\n * escaped with a preceding backslash anywhere they appear as literal text.\n */\nconst MARKDOWN_V2_RESERVED = '_*[]()~`>#+-=|{}.!'\n\n/** Escape literal text for Telegram MarkdownV2. */\nexport function escapeMarkdownV2(text: string): string {\n let out = ''\n for (const char of text) {\n if (MARKDOWN_V2_RESERVED.includes(char)) out += '\\\\'\n out += char\n }\n return out\n}\n\n/**\n * Escape the URL inside a MarkdownV2 inline link `[label](url)`.\n * Inside the parentheses only `)` and `\\` are special, and escaping the full\n * reserved set here would corrupt query strings and path separators.\n */\nexport function escapeMarkdownV2Url(url: string): string {\n return url.replace(/[\\\\)]/g, (char) => `\\\\${char}`)\n}\n\n/**\n * Escape text inside a MarkdownV2 code span or `pre` block. Only `` ` `` and\n * `\\` are special there. Escaping the full set would print literal backslashes\n * to the user, which is the classic over-correction of this bug.\n */\nexport function escapeMarkdownV2Code(text: string): string {\n return text.replace(/[\\\\`]/g, (char) => `\\\\${char}`)\n}\n\n/** Build a MarkdownV2 inline link with both halves escaped correctly. */\nexport function markdownV2Link(label: string, url: string): string {\n return `[${escapeMarkdownV2(label)}](${escapeMarkdownV2Url(url)})`\n}\n\n/**\n * Escape text for Telegram HTML parse mode. Telegram's HTML is a small subset\n * and requires exactly `&`, `<` and `>` to be replaced in text nodes.\n */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n/** Escape a value going into an HTML attribute (a link `href`). */\nexport function escapeHtmlAttribute(value: string): string {\n return escapeHtml(value).replace(/\"/g, '&quot;')\n}\n\n/** Build a Telegram HTML anchor with both halves escaped. */\nexport function htmlLink(label: string, url: string): string {\n return `<a href=\"${escapeHtmlAttribute(url)}\">${escapeHtml(label)}</a>`\n}\n\n/**\n * Discord markdown special characters. Discord renders markdown inside embed\n * titles, descriptions and field values, so untrusted token names need the\n * same treatment. `#` is not escaped: it is only special at the start of a\n * line for headings, and every place we interpolate a name is mid-line.\n */\nconst DISCORD_RESERVED = /[\\\\*_~`|>[\\]()]/g\n\n/** Escape literal text for Discord markdown rendering. */\nexport function escapeDiscordMarkdown(text: string): string {\n return text.replace(DISCORD_RESERVED, (char) => `\\\\${char}`)\n}\n\n/**\n * Truncate to a maximum length without splitting a surrogate pair, which is\n * what turns a long emoji-bearing token name into a replacement character.\n * The ellipsis is included in the budget.\n */\nexport function truncate(text: string, max: number): string {\n if (max <= 0) return ''\n const chars = [...text]\n if (chars.length <= max) return text\n if (max === 1) return '…'\n return `${chars.slice(0, max - 1).join('')}…`\n}\n\n/**\n * A display label for a token: its symbol when the contract exposed one,\n * otherwise a shortened address. Never returns an empty string, because an\n * empty Telegram entity is a 400.\n */\nexport function displayLabel(symbol: string | null, address: string): string {\n const trimmed = symbol?.trim()\n if (trimmed) return truncate(trimmed, 40)\n return shortAddress(address)\n}\n\n/** `0x1234…abcd`, the standard compact address rendering. */\nexport function shortAddress(address: string): string {\n if (address.length <= 12) return address\n return `${address.slice(0, 6)}…${address.slice(-4)}`\n}\n","import { formatUnits } from 'viem'\nimport type { AlertEvent } from '../events/types.js'\nimport {\n displayLabel,\n escapeDiscordMarkdown,\n escapeHtml,\n escapeMarkdownV2,\n htmlLink,\n markdownV2Link,\n shortAddress,\n truncate,\n} from './escape.js'\nimport type { DiscordEmbed, DiscordEmbedField, RenderedAlert } from './types.js'\n\n/**\n * Message formatting, one alert at a time, for both platforms.\n *\n * Every piece of chain-derived text (token name, symbol) goes through the\n * platform's escaper. Every piece of package-authored text (labels, headings)\n * is written to be escape-safe already, so the output stays readable instead\n * of being a wall of backslashes.\n */\n\n/** Colour per event kind, as Discord's decimal RGB. */\nexport const EMBED_COLORS = {\n launch: 0x22c55e,\n curve_trade: 0x38bdf8,\n graduation: 0xa855f7,\n whale_trade: 0xf59e0b,\n} as const\n\nconst KIND_EMOJI = {\n launch: '🚀',\n curve_trade: '📈',\n graduation: '🎓',\n whale_trade: '🐋',\n} as const\n\nconst LAUNCHPAD_NAMES = { noxa: 'NOXA', odyssey: 'The Odyssey' } as const\n\nconst usdFormatter = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 2,\n})\n\n/** Compact USD rendering: `$1,234.50`, `$12.4K`, `$3.1M`. */\nexport function formatUsd(value: number | null): string {\n if (value === null || !Number.isFinite(value)) return 'unknown'\n const abs = Math.abs(value)\n if (abs >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M`\n if (abs >= 10_000) return `$${(value / 1_000).toFixed(1)}K`\n return usdFormatter.format(value)\n}\n\n/** Human amount rendering with sane precision across 12 orders of magnitude. */\nexport function formatAmount(value: number): string {\n const abs = Math.abs(value)\n if (abs === 0) return '0'\n if (abs >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(2)}B`\n if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`\n if (abs >= 1_000) return `${(value / 1_000).toFixed(2)}K`\n if (abs >= 1) return value.toFixed(4).replace(/\\.?0+$/, '')\n return value.toPrecision(4)\n}\n\n/** ETH amount from wei, with a trailing `ETH`. */\nexport function formatEth(wei: bigint): string {\n return `${formatAmount(Number(formatUnits(wei < 0n ? -wei : wei, 18)))} ETH`\n}\n\n/** The one-line headline for an event, before escaping. */\nexport function headline(event: AlertEvent): string {\n const label = displayLabel(event.symbol, event.token)\n const pad = LAUNCHPAD_NAMES[event.launchpad]\n switch (event.kind) {\n case 'launch':\n return `${KIND_EMOJI.launch} New launch: ${label} on ${pad}`\n case 'curve_trade':\n return `${KIND_EMOJI.curve_trade} Curve ${event.side}: ${label} for ${formatUsd(event.usdValue)}`\n case 'graduation':\n return `${KIND_EMOJI.graduation} Graduated: ${label} filled its curve`\n case 'whale_trade':\n return `${KIND_EMOJI.whale_trade} Whale ${event.side}: ${formatUsd(event.usdValue)} of ${label}`\n }\n}\n\n/** Label/value pairs describing an event, before escaping. */\nexport function detailRows(event: AlertEvent): { label: string; value: string; inline?: boolean }[] {\n const rows: { label: string; value: string; inline?: boolean }[] = []\n rows.push({ label: 'Launchpad', value: LAUNCHPAD_NAMES[event.launchpad], inline: true })\n if (event.name) rows.push({ label: 'Name', value: truncate(event.name, 64), inline: true })\n rows.push({ label: 'Token', value: shortAddress(event.token), inline: true })\n\n switch (event.kind) {\n case 'launch': {\n rows.push({\n label: 'Listing',\n value: event.instantListing\n ? 'Instant (live Uniswap v3 pool, no curve)'\n : 'Bonding curve (graduates when filled)',\n inline: true,\n })\n rows.push({ label: 'Deployer', value: shortAddress(event.actor), inline: true })\n if (event.initialBuyAmount > 0n) {\n rows.push({ label: 'Initial buy', value: formatUsd(event.usdValue), inline: true })\n }\n break\n }\n case 'curve_trade': {\n rows.push({ label: 'Side', value: event.side === 'buy' ? 'Buy' : 'Sell', inline: true })\n rows.push({ label: 'Size', value: formatEth(event.quoteAmountWei), inline: true })\n rows.push({ label: 'Value', value: formatUsd(event.usdValue), inline: true })\n rows.push({\n label: 'Tokens',\n value: formatAmount(Number(formatUnits(event.tokenAmount, 18))),\n inline: true,\n })\n rows.push({ label: 'Trader', value: shortAddress(event.actor), inline: true })\n break\n }\n case 'graduation': {\n rows.push({ label: 'Pool', value: shortAddress(event.pool), inline: true })\n rows.push({ label: 'Seeded', value: formatUsd(event.usdValue), inline: true })\n rows.push({ label: 'LP position', value: `#${event.positionId}`, inline: true })\n break\n }\n case 'whale_trade': {\n rows.push({ label: 'Side', value: event.side === 'buy' ? 'Buy' : 'Sell', inline: true })\n rows.push({ label: 'Value', value: formatUsd(event.usdValue), inline: true })\n rows.push({\n label: 'Size',\n value: `${formatAmount(event.tokenAmount)} for ${formatAmount(event.quoteAmount)} ${event.quoteSymbol}`,\n inline: true,\n })\n rows.push({ label: 'Price', value: `${formatAmount(event.price)} ${event.quoteSymbol}`, inline: true })\n rows.push({ label: 'Fee tier', value: `${event.feeTier / 10_000}%`, inline: true })\n rows.push({ label: 'Trader', value: shortAddress(event.actor), inline: true })\n break\n }\n }\n return rows\n}\n\n/** Options for {@link renderAlert}. */\nexport interface RenderOptions {\n /**\n * Telegram parse mode. HTML is the default: its escape rules have three\n * special characters instead of eighteen, which makes malformed-entity 400s\n * far less likely on adversarial token names.\n * @defaultValue `'HTML'`\n */\n parseMode?: 'HTML' | 'MarkdownV2'\n /** Footer text, e.g. the rule that matched. */\n footer?: string\n}\n\n/**\n * Render an event for both platforms.\n *\n * @example\n * ```ts\n * const alert = renderAlert(event, { footer: 'rule: whales' })\n * await telegram.send('123456789', alert)\n * await discord.send('987654321', alert)\n * ```\n */\nexport function renderAlert(event: AlertEvent, options: RenderOptions = {}): RenderedAlert {\n const parseMode = options.parseMode ?? 'HTML'\n const title = headline(event)\n const rows = detailRows(event)\n const label = displayLabel(event.symbol, event.token)\n\n const linkPairs: { label: string; url: string }[] = [\n { label: 'Transaction', url: event.explorer.tx },\n { label: 'Token', url: event.explorer.token },\n ]\n if (event.explorer.pool) linkPairs.push({ label: 'Pool', url: event.explorer.pool })\n linkPairs.push({ label: 'Actor', url: event.explorer.actor })\n\n const text =\n parseMode === 'HTML'\n ? [\n `<b>${escapeHtml(title)}</b>`,\n '',\n ...rows.map((row) => `${escapeHtml(row.label)}: <b>${escapeHtml(row.value)}</b>`),\n `Contract: <code>${escapeHtml(event.token)}</code>`,\n '',\n linkPairs.map((link) => htmlLink(link.label, link.url)).join(' · '),\n ...(options.footer ? ['', `<i>${escapeHtml(options.footer)}</i>`] : []),\n ].join('\\n')\n : [\n `*${escapeMarkdownV2(title)}*`,\n '',\n ...rows.map((row) => `${escapeMarkdownV2(row.label)}: *${escapeMarkdownV2(row.value)}*`),\n `Contract: \\`${event.token}\\``,\n '',\n linkPairs.map((link) => markdownV2Link(link.label, link.url)).join(' · '),\n ...(options.footer ? ['', `_${escapeMarkdownV2(options.footer)}_`] : []),\n ].join('\\n')\n\n const fields: DiscordEmbedField[] = rows.map((row) => ({\n name: truncate(row.label, 256),\n value: truncate(escapeDiscordMarkdown(row.value), 1024),\n inline: row.inline ?? false,\n }))\n fields.push({\n name: 'Links',\n value: linkPairs.map((link) => `[${link.label}](${link.url})`).join(' · '),\n inline: false,\n })\n\n const embed: DiscordEmbed = {\n title: truncate(escapeDiscordMarkdown(title), 256),\n url: event.explorer.tx,\n description: `\\`${event.token}\\``,\n color: EMBED_COLORS[event.kind],\n fields,\n ...(options.footer ? { footer: { text: truncate(options.footer, 2048) } } : {}),\n ...(event.timestampMs !== null\n ? { timestamp: new Date(event.timestampMs).toISOString() }\n : {}),\n }\n\n const plain = `${title} | ${label} ${event.token} | block ${event.blockNumber} | ${event.explorer.tx}`\n\n return { text, parseMode, embed, plain }\n}\n","import type { FetchLike, FetchLikeResponse } from './types.js'\n\n/** A completed HTTP response with its body parsed as JSON when possible. */\nexport interface JsonResponse {\n status: number\n ok: boolean\n headers: { get(name: string): string | null }\n body: unknown\n raw: string\n}\n\n/** Default sleep. */\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)))\n\n/** Resolve the `fetch` to use: the injected double in tests, the global otherwise. */\nexport function resolveFetch(injected?: FetchLike): FetchLike {\n if (injected) return injected\n if (typeof globalThis.fetch !== 'function') {\n throw new Error('hood-alerts: global fetch is unavailable (Node 20+ required), and none was injected')\n }\n return globalThis.fetch as unknown as FetchLike\n}\n\n/**\n * POST JSON and parse the response.\n *\n * A non-JSON body (an HTML error page from a proxy, an empty 204) is not an\n * error here: `body` becomes `null` and `raw` keeps the text, so callers can\n * report what actually came back instead of a `SyntaxError` from deep inside\n * the adapter.\n */\nexport async function postJson(\n fetchImpl: FetchLike,\n url: string,\n payload: unknown,\n headers: Record<string, string> = {},\n): Promise<JsonResponse> {\n const response: FetchLikeResponse = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(payload),\n })\n const raw = await response.text()\n let body: unknown = null\n if (raw) {\n try {\n body = JSON.parse(raw)\n } catch {\n body = null\n }\n }\n return { status: response.status, ok: response.ok, headers: response.headers, body, raw }\n}\n\n/** Read a numeric header, or `null` when absent or unparseable. */\nexport function numericHeader(\n headers: { get(name: string): string | null },\n name: string,\n): number | null {\n const value = headers.get(name)\n if (value === null) return null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/** Read a nested property from an unknown JSON body without casting blindly. */\nexport function pick(body: unknown, ...path: string[]): unknown {\n let current: unknown = body\n for (const key of path) {\n if (typeof current !== 'object' || current === null) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n","import type { Platform } from '../rules/schema.js'\n\n/** A Discord embed field. */\nexport interface DiscordEmbedField {\n name: string\n value: string\n inline?: boolean\n}\n\n/** The subset of the Discord embed object this package produces. */\nexport interface DiscordEmbed {\n title: string\n url?: string\n description?: string\n /** Decimal RGB, per Discord's API. */\n color: number\n fields: DiscordEmbedField[]\n footer?: { text: string }\n /** ISO-8601 timestamp. Omitted when the block time was not read. */\n timestamp?: string\n}\n\n/**\n * One alert, rendered for every platform up front.\n *\n * Rendering once and letting each notifier pick its representation keeps the\n * escaping rules next to each other (where they can be compared and tested\n * together) and means a fan-out to Telegram and Discord formats the event\n * once, not twice.\n */\nexport interface RenderedAlert {\n /** Telegram body, escaped for {@link RenderedAlert.parseMode}. */\n text: string\n parseMode: 'HTML' | 'MarkdownV2'\n /** Discord embed representation of the same alert. */\n embed: DiscordEmbed\n /** Unstyled one-line summary, for logs and for platforms added later. */\n plain: string\n}\n\n/** What happened when a notifier tried to deliver. */\nexport interface DeliveryResult {\n ok: boolean\n /** HTTP status of the final attempt, or `null` if the request never completed. */\n status: number | null\n /** How many requests were made, including the successful one. */\n attempts: number\n /** Platform message id, when the API returned one. */\n messageId: string | null\n /** Failure detail. `null` on success. */\n error: string | null\n /**\n * `true` when the failure is worth retrying later (5xx, network, exhausted\n * rate limit). `false` for a permanent rejection such as a bad chat id, so\n * the service can disable a dead subscription instead of retrying forever.\n */\n retryable: boolean\n}\n\n/** The one interface every delivery adapter implements. */\nexport interface Notifier {\n readonly platform: Platform\n /** Adapter flavour, for logs and metrics: `bot`, `webhook`. */\n readonly kind: string\n /**\n * Deliver a rendered alert to a platform-specific target: a Telegram chat\n * id, a Discord channel id, or a Discord webhook URL.\n */\n send(target: string, alert: RenderedAlert): Promise<DeliveryResult>\n}\n\n/** The `fetch` shape the adapters use, so tests can substitute a double. */\nexport type FetchLike = (\n input: string,\n init?: { method?: string; headers?: Record<string, string>; body?: string },\n) => Promise<FetchLikeResponse>\n\n/** The response shape the adapters read. */\nexport interface FetchLikeResponse {\n ok: boolean\n status: number\n headers: { get(name: string): string | null }\n text(): Promise<string>\n}\n\n/** Options common to every HTTP-backed notifier. */\nexport interface NotifierHttpOptions {\n /** Injected `fetch`. Defaults to the global. */\n fetch?: FetchLike\n /** Max attempts per delivery, including the first. @defaultValue `4` */\n maxAttempts?: number\n /** Base backoff in ms for retryable failures, doubled per attempt. @defaultValue `500` */\n backoffMs?: number\n /**\n * Upper bound on how long a single delivery will wait for a rate limit, in\n * ms. A `retry_after` beyond this gives up and reports a retryable failure\n * rather than blocking the whole dispatch loop.\n * @defaultValue `60_000`\n */\n maxWaitMs?: number\n /** Sleep injection point for tests. */\n sleep?: (ms: number) => Promise<void>\n /** Request timeout in ms. @defaultValue `15_000` */\n timeoutMs?: number\n}\n\n/** A successful send result. */\nexport function delivered(status: number, attempts: number, messageId: string | null): DeliveryResult {\n return { ok: true, status, attempts, messageId, error: null, retryable: false }\n}\n\n/** A failed send result. */\nexport function failed(\n status: number | null,\n attempts: number,\n error: string,\n retryable: boolean,\n): DeliveryResult {\n return { ok: false, status, attempts, messageId: null, error, retryable }\n}\n","import { numericHeader, pick, postJson, resolveFetch, sleep as realSleep } from './http.js'\nimport {\n delivered,\n failed,\n type DeliveryResult,\n type FetchLike,\n type Notifier,\n type NotifierHttpOptions,\n type RenderedAlert,\n} from './types.js'\n\n/**\n * Telegram Bot API adapter.\n *\n * Rate limiting is the part everyone gets wrong. Telegram does not use a\n * header budget: it answers `429` with a JSON body\n * `{\"ok\":false,\"error_code\":429,\"description\":\"Too Many Requests: retry after 7\",\n * \"parameters\":{\"retry_after\":7}}` and expects the caller to wait exactly that\n * many *seconds* before retrying. This adapter honours `parameters.retry_after`\n * (falling back to the `Retry-After` header, then to exponential backoff), caps\n * the total wait at `maxWaitMs` so one throttled chat cannot stall the whole\n * dispatch loop, and treats `400`/`403` as permanent so a deleted chat or a\n * blocked bot stops being retried forever.\n */\n\n/** A raw Telegram Bot API response. */\nexport interface TelegramResponse {\n ok: boolean\n status: number\n /** `result` on success. */\n result: unknown\n /** `description` on failure. */\n description: string | null\n /** `parameters.retry_after` in seconds, when Telegram sent one. */\n retryAfterSeconds: number | null\n}\n\n/** Options for {@link createTelegramClient}. */\nexport interface TelegramClientOptions extends NotifierHttpOptions {\n /** Bot token from @BotFather, `<id>:<secret>`. */\n botToken: string\n /** API origin. Override for a local Bot API server. @defaultValue `https://api.telegram.org` */\n apiBase?: string\n}\n\n/** A thin, rate-limit-aware Telegram Bot API client. */\nexport interface TelegramClient {\n /** Call any Bot API method. Retries 429 and 5xx, never retries 4xx. */\n call(method: string, params: Record<string, unknown>): Promise<TelegramResponse>\n /** How many requests the last {@link TelegramClient.call} needed. */\n readonly lastAttempts: number\n}\n\nconst PERMANENT_STATUSES = new Set([400, 401, 403, 404])\n\n/** Build a Telegram Bot API client. */\nexport function createTelegramClient(options: TelegramClientOptions): TelegramClient {\n if (!/^\\d+:[A-Za-z0-9_-]+$/.test(options.botToken)) {\n throw new Error('hood-alerts: TELEGRAM_BOT_TOKEN must look like \"<bot id>:<secret>\"')\n }\n const fetchImpl: FetchLike = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://api.telegram.org').replace(/\\/+$/, '')\n const maxAttempts = options.maxAttempts ?? 4\n const backoffMs = options.backoffMs ?? 500\n const maxWaitMs = options.maxWaitMs ?? 60_000\n const sleep = options.sleep ?? realSleep\n\n let lastAttempts = 0\n\n return {\n get lastAttempts() {\n return lastAttempts\n },\n async call(method: string, params: Record<string, unknown>): Promise<TelegramResponse> {\n const url = `${apiBase}/bot${options.botToken}/${method}`\n let attempt = 0\n let last: TelegramResponse = {\n ok: false,\n status: 0,\n result: null,\n description: 'no attempt was made',\n retryAfterSeconds: null,\n }\n\n while (attempt < maxAttempts) {\n attempt += 1\n lastAttempts = attempt\n let response\n try {\n response = await postJson(fetchImpl, url, params)\n } catch (error) {\n // Transport failure: retry with backoff, it is not a Telegram verdict.\n last = {\n ok: false,\n status: 0,\n result: null,\n description: error instanceof Error ? error.message : String(error),\n retryAfterSeconds: null,\n }\n if (attempt >= maxAttempts) break\n await sleep(backoffMs * 2 ** (attempt - 1))\n continue\n }\n\n const description = typeof pick(response.body, 'description') === 'string'\n ? (pick(response.body, 'description') as string)\n : null\n const retryAfterField = pick(response.body, 'parameters', 'retry_after')\n const retryAfterSeconds =\n typeof retryAfterField === 'number'\n ? retryAfterField\n : numericHeader(response.headers, 'retry-after')\n\n last = {\n ok: response.status === 200 && pick(response.body, 'ok') === true,\n status: response.status,\n result: pick(response.body, 'result') ?? null,\n description,\n retryAfterSeconds,\n }\n if (last.ok) return last\n\n if (response.status === 429) {\n const waitMs = (retryAfterSeconds ?? backoffMs / 1000) * 1000\n if (waitMs > maxWaitMs || attempt >= maxAttempts) return last\n await sleep(waitMs)\n continue\n }\n if (PERMANENT_STATUSES.has(response.status)) return last\n if (attempt >= maxAttempts) return last\n await sleep(backoffMs * 2 ** (attempt - 1))\n }\n return last\n },\n }\n}\n\n/** Options for {@link createTelegramNotifier}. */\nexport interface TelegramNotifierOptions extends TelegramClientOptions {\n /** Suppress link previews. @defaultValue `true` (alerts carry several links). */\n disableWebPagePreview?: boolean\n /** Deliver silently, without a notification sound. @defaultValue `false` */\n disableNotification?: boolean\n /** Reuse an existing client instead of building one (the bot shares its client). */\n client?: TelegramClient\n}\n\n/**\n * Build the Telegram delivery adapter.\n *\n * @example\n * ```ts\n * const telegram = createTelegramNotifier({ botToken: process.env.TELEGRAM_BOT_TOKEN! })\n * const result = await telegram.send('-1001234567890', renderAlert(event))\n * if (!result.ok) console.error(result.error, 'retryable:', result.retryable)\n * ```\n */\nexport function createTelegramNotifier(options: TelegramNotifierOptions): Notifier {\n const client = options.client ?? createTelegramClient(options)\n const disableWebPagePreview = options.disableWebPagePreview ?? true\n const disableNotification = options.disableNotification ?? false\n\n return {\n platform: 'telegram',\n kind: 'bot',\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n if (!/^-?\\d+$|^@[A-Za-z][A-Za-z0-9_]{4,}$/.test(target)) {\n return failed(\n null,\n 0,\n `invalid Telegram chat id \"${target}\": expected a numeric id or an @username`,\n false,\n )\n }\n const response = await client.call('sendMessage', {\n chat_id: /^-?\\d+$/.test(target) ? Number(target) : target,\n text: alert.text,\n parse_mode: alert.parseMode,\n link_preview_options: { is_disabled: disableWebPagePreview },\n disable_notification: disableNotification,\n })\n const attempts = client.lastAttempts\n\n if (response.ok) {\n const messageId = pick(response.result, 'message_id')\n return delivered(response.status, attempts, messageId === undefined ? null : String(messageId))\n }\n const retryable = response.status === 429 || response.status >= 500 || response.status === 0\n return failed(\n response.status || null,\n attempts,\n response.description ?? `Telegram sendMessage failed with status ${response.status}`,\n retryable,\n )\n },\n }\n}\n","import { numericHeader, pick, postJson, resolveFetch, sleep as realSleep } from './http.js'\nimport {\n delivered,\n failed,\n type DeliveryResult,\n type FetchLike,\n type Notifier,\n type NotifierHttpOptions,\n type RenderedAlert,\n} from './types.js'\n\n/**\n * Discord adapters: webhook and bot (application) delivery, behind the same\n * {@link Notifier} interface.\n *\n * Discord's rate limiting is a per-route bucket, published on every response:\n * `X-RateLimit-Remaining` and `X-RateLimit-Reset-After` (seconds, fractional).\n * The correct behaviour is to *not send* once a bucket is exhausted rather\n * than to send and absorb the 429, so this adapter tracks the bucket per route\n * and waits before the request. A 429 that still slips through (a global limit,\n * or another process sharing the token) is honoured through the `retry_after`\n * field in the JSON body, which unlike Telegram's is in **seconds as a float**.\n * `X-RateLimit-Global` marks a token-wide limit, which is applied to every\n * route rather than only the one that hit it.\n */\n\ninterface Bucket {\n remaining: number\n resetAtMs: number\n}\n\n/** Shared rate-limit state for one Discord credential. */\nexport interface DiscordRateLimiter {\n /** Wait until `route` may be called. */\n acquire(route: string): Promise<void>\n /** Record what a response said about the bucket. */\n update(route: string, headers: { get(name: string): string | null }): void\n /** Apply a 429, globally when Discord says so. */\n penalise(route: string, retryAfterMs: number, global: boolean): void\n}\n\n/** Build the shared rate-limit tracker. */\nexport function createDiscordRateLimiter(\n now: () => number = Date.now,\n sleep: (ms: number) => Promise<void> = realSleep,\n): DiscordRateLimiter {\n const buckets = new Map<string, Bucket>()\n let globalResetAtMs = 0\n\n return {\n async acquire(route: string): Promise<void> {\n const globalWait = globalResetAtMs - now()\n if (globalWait > 0) await sleep(globalWait)\n const bucket = buckets.get(route)\n if (!bucket) return\n if (bucket.remaining > 0) return\n const wait = bucket.resetAtMs - now()\n if (wait > 0) await sleep(wait)\n },\n update(route: string, headers: { get(name: string): string | null }): void {\n const remaining = numericHeader(headers, 'x-ratelimit-remaining')\n const resetAfter = numericHeader(headers, 'x-ratelimit-reset-after')\n if (remaining === null && resetAfter === null) return\n buckets.set(route, {\n remaining: remaining ?? 1,\n resetAtMs: now() + (resetAfter ?? 0) * 1000,\n })\n },\n penalise(route: string, retryAfterMs: number, global: boolean): void {\n const resetAtMs = now() + retryAfterMs\n if (global) globalResetAtMs = Math.max(globalResetAtMs, resetAtMs)\n buckets.set(route, { remaining: 0, resetAtMs })\n },\n }\n}\n\n/** Options shared by both Discord adapters. */\nexport interface DiscordOptionsBase extends NotifierHttpOptions {\n /** Share one limiter across adapters that use the same credential. */\n limiter?: DiscordRateLimiter\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n /** Username override for webhook posts. */\n username?: string\n}\n\nconst PERMANENT_STATUSES = new Set([400, 401, 403, 404, 405])\n\ninterface SendArgs {\n url: string\n route: string\n payload: unknown\n headers: Record<string, string>\n}\n\nasync function deliver(\n args: SendArgs,\n options: DiscordOptionsBase,\n fetchImpl: FetchLike,\n limiter: DiscordRateLimiter,\n): Promise<DeliveryResult> {\n const maxAttempts = options.maxAttempts ?? 4\n const backoffMs = options.backoffMs ?? 500\n const maxWaitMs = options.maxWaitMs ?? 60_000\n const sleep = options.sleep ?? realSleep\n\n let attempt = 0\n let lastStatus: number | null = null\n let lastError = 'no attempt was made'\n let retryable = true\n\n while (attempt < maxAttempts) {\n attempt += 1\n await limiter.acquire(args.route)\n\n let response\n try {\n response = await postJson(fetchImpl, args.url, args.payload, args.headers)\n } catch (error) {\n lastStatus = null\n lastError = error instanceof Error ? error.message : String(error)\n retryable = true\n if (attempt >= maxAttempts) break\n await sleep(backoffMs * 2 ** (attempt - 1))\n continue\n }\n\n limiter.update(args.route, response.headers)\n lastStatus = response.status\n\n if (response.status === 429) {\n const retryAfterField = pick(response.body, 'retry_after')\n const retryAfterSeconds =\n typeof retryAfterField === 'number'\n ? retryAfterField\n : (numericHeader(response.headers, 'retry-after') ?? backoffMs / 1000)\n const isGlobal =\n pick(response.body, 'global') === true ||\n response.headers.get('x-ratelimit-global') === 'true'\n const waitMs = retryAfterSeconds * 1000\n limiter.penalise(args.route, waitMs, isGlobal)\n lastError = `rate limited, retry after ${retryAfterSeconds}s${isGlobal ? ' (global)' : ''}`\n retryable = true\n if (waitMs > maxWaitMs || attempt >= maxAttempts) break\n await sleep(waitMs)\n continue\n }\n\n if (response.ok) {\n // Webhooks answer 204 with an empty body unless `wait=true` was used.\n const messageId = pick(response.body, 'id')\n return delivered(response.status, attempt, messageId === undefined ? null : String(messageId))\n }\n\n const message =\n (typeof pick(response.body, 'message') === 'string'\n ? (pick(response.body, 'message') as string)\n : null) ?? response.raw.slice(0, 200)\n lastError = message || `Discord request failed with status ${response.status}`\n\n if (PERMANENT_STATUSES.has(response.status)) {\n retryable = false\n break\n }\n retryable = true\n if (attempt >= maxAttempts) break\n await sleep(backoffMs * 2 ** (attempt - 1))\n }\n\n return failed(lastStatus, attempt, lastError, retryable)\n}\n\n/** Options for {@link createDiscordWebhookNotifier}. */\nexport interface DiscordWebhookOptions extends DiscordOptionsBase {\n /**\n * Default webhook URL. Optional: `send()` accepts a webhook URL as its\n * target, which is how per-subscription webhooks work.\n */\n webhookUrl?: string\n}\n\nconst WEBHOOK_PATTERN = /^https:\\/\\/(?:\\w+\\.)?discord(?:app)?\\.com\\/api(?:\\/v\\d+)?\\/webhooks\\/\\d+\\/[\\w-]+$/\n\n/**\n * Webhook delivery. No bot account, no gateway, no permissions setup: the\n * subscriber pastes a channel webhook URL and alerts arrive. This is the\n * lowest-friction Discord path and the one most subscriptions use.\n *\n * @example\n * ```ts\n * const discord = createDiscordWebhookNotifier()\n * await discord.send('https://discord.com/api/webhooks/123/abc', renderAlert(event))\n * ```\n */\nexport function createDiscordWebhookNotifier(options: DiscordWebhookOptions = {}): Notifier {\n const fetchImpl = resolveFetch(options.fetch)\n const limiter =\n options.limiter ?? createDiscordRateLimiter(options.now ?? Date.now, options.sleep ?? realSleep)\n\n return {\n platform: 'discord',\n kind: 'webhook',\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n const url = target || options.webhookUrl\n if (!url || !WEBHOOK_PATTERN.test(url)) {\n return failed(\n null,\n 0,\n `invalid Discord webhook URL \"${target}\": expected https://discord.com/api/webhooks/<id>/<token>`,\n false,\n )\n }\n return deliver(\n {\n url: `${url}?wait=true`,\n // Discord buckets webhook routes per webhook id.\n route: `webhook:${url.split('/').slice(-2, -1)[0] ?? url}`,\n payload: {\n embeds: [alert.embed],\n ...(options.username ? { username: options.username } : {}),\n },\n headers: {},\n },\n options,\n fetchImpl,\n limiter,\n )\n },\n }\n}\n\n/** Options for {@link createDiscordBotNotifier}. */\nexport interface DiscordBotOptions extends DiscordOptionsBase {\n /** Bot token from the Discord developer portal (no `Bot ` prefix). */\n botToken: string\n /** API base. @defaultValue `https://discord.com/api/v10` */\n apiBase?: string\n}\n\n/**\n * Bot delivery: posts to a channel id with the application's bot token, which\n * is what slash-command subscriptions use (the bot is already in the guild, so\n * no webhook needs creating).\n *\n * @example\n * ```ts\n * const discord = createDiscordBotNotifier({ botToken: process.env.DISCORD_BOT_TOKEN! })\n * await discord.send('1234567890', renderAlert(event))\n * ```\n */\nexport function createDiscordBotNotifier(options: DiscordBotOptions): Notifier {\n if (!options.botToken.trim()) {\n throw new Error('hood-alerts: DISCORD_BOT_TOKEN is required for the Discord bot notifier')\n }\n const fetchImpl = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://discord.com/api/v10').replace(/\\/+$/, '')\n const limiter =\n options.limiter ?? createDiscordRateLimiter(options.now ?? Date.now, options.sleep ?? realSleep)\n\n return {\n platform: 'discord',\n kind: 'bot',\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n if (!/^\\d{5,25}$/.test(target)) {\n return failed(null, 0, `invalid Discord channel id \"${target}\": expected a numeric snowflake`, false)\n }\n return deliver(\n {\n url: `${apiBase}/channels/${target}/messages`,\n route: `channel:${target}`,\n payload: { embeds: [alert.embed] },\n headers: { authorization: `Bot ${options.botToken}` },\n },\n options,\n fetchImpl,\n limiter,\n )\n },\n }\n}\n","import type { Platform } from '../rules/schema.js'\nimport { delivered, type DeliveryResult, type Notifier, type RenderedAlert } from './types.js'\n\n/** One captured delivery. */\nexport interface CapturedDelivery {\n platform: Platform\n target: string\n alert: RenderedAlert\n atMs: number\n}\n\n/** A notifier that records deliveries instead of sending them. */\nexport interface CaptureNotifier extends Notifier {\n /** Everything captured so far, in order. */\n readonly sent: readonly CapturedDelivery[]\n /** Drop the captured history. */\n clear(): void\n}\n\n/**\n * A delivery adapter that records instead of sending.\n *\n * This is a first-class operating mode, not a stub: running the service with\n * `DRY_RUN=1` swaps the real adapters for this one, so an operator can point a\n * fresh deployment at mainnet and watch exactly which alerts *would* have gone\n * out, with the real rendered text, before handing it bot tokens. It is also\n * what the notifier tests assert against.\n *\n * @example\n * ```ts\n * const capture = createCaptureNotifier('telegram')\n * await capture.send('-1001234567890', renderAlert(event))\n * console.log(capture.sent[0]?.alert.text)\n * ```\n */\nexport function createCaptureNotifier(\n platform: Platform,\n onSend?: (delivery: CapturedDelivery) => void,\n now: () => number = Date.now,\n): CaptureNotifier {\n const sent: CapturedDelivery[] = []\n return {\n platform,\n kind: 'capture',\n sent,\n clear(): void {\n sent.length = 0\n },\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n const delivery: CapturedDelivery = { platform, target, alert, atMs: now() }\n sent.push(delivery)\n onSend?.(delivery)\n return delivered(200, 1, `capture-${sent.length}`)\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport { getAddress } from 'viem'\nimport { escapeHtml } from '../notifiers/escape.js'\nimport {\n defaultRules,\n formatValidationError,\n parseSubscription,\n ruleComplexity,\n safeParseRule,\n type Platform,\n type Rule,\n type Subscription,\n} from '../rules/schema.js'\nimport { linkMessage, verifyWalletLink } from '../tiers/entitlements.js'\nimport type { EntitlementGate } from '../tiers/enforce.js'\nimport type { AlertStore } from '../service/store.js'\n\n/**\n * The bot's command surface, written once and shared by Telegram and Discord.\n *\n * Both platforms deliver the same thing to this router: a command name, a\n * subscriber id, a default delivery target and a list of arguments. Keeping\n * the logic here means the two bots cannot drift apart in what `/rule add`\n * accepts or what `/tier` reports, and it means the command tests exercise the\n * real code path rather than a Telegram-shaped copy of it.\n *\n * Every command answers bad input with a specific, actionable message. There\n * is no silent failure and no raw validation dump: a mistyped threshold gets\n * \"usd must be a positive number, for example /threshold whales 5000\".\n */\n\n/** What a platform hands the router. */\nexport interface CommandContext {\n /** Stable identity for entitlements: `telegram:<user id>`, `discord:<guild or user id>`. */\n subscriberId: string\n platform: Platform\n /**\n * Where alerts would go if the user does not name a target: the Telegram\n * chat id, or the Discord channel id the command was used in.\n */\n defaultTarget: string\n /** Arguments after the command word. */\n args: string[]\n /** The whole argument string, for commands that take free-form JSON. */\n rest: string\n}\n\n/** A reply to render back to the user. */\nexport interface CommandReply {\n /** Telegram HTML, which Discord renders acceptably after tag stripping. */\n html: string\n /** Plain-text equivalent, used by Discord and by logs. */\n text: string\n}\n\n/** A command the bot exposes. */\nexport interface CommandSpec {\n name: string\n description: string\n /** Usage line shown by `/help` and in the README. */\n usage: string\n /** Discord slash-command options, when the command takes arguments. */\n options?: { name: string; description: string; required: boolean }[]\n}\n\n/** Everything the router needs. */\nexport interface CommandRouterDeps {\n store: AlertStore\n gate: EntitlementGate\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n /** Id factory for new subscriptions and nonces. @defaultValue `randomUUID` */\n newId?: () => string\n /** Shown by `/upgrade`: how to pay. Set from the service config. */\n upgradeInstructions?: string\n}\n\n/** The router. */\nexport interface CommandRouter {\n /** Every command, for `/help` and for Discord registration. */\n readonly commands: readonly CommandSpec[]\n /** Handle one command. Never throws: errors come back as a reply. */\n handle(command: string, context: CommandContext): Promise<CommandReply>\n}\n\nconst reply = (text: string): CommandReply => ({ html: escapeHtml(text), text })\n\nconst html = (htmlText: string, plain: string): CommandReply => ({ html: htmlText, text: plain })\n\n/** The command catalogue. */\nexport const COMMANDS: readonly CommandSpec[] = [\n { name: 'start', description: 'Set up alerts in this chat', usage: '/start' },\n { name: 'help', description: 'Show every command', usage: '/help' },\n {\n name: 'subscribe',\n description: 'Send alerts here, with a starter rule set',\n usage: '/subscribe [target]',\n options: [\n {\n name: 'target',\n description: 'Discord webhook URL or channel id. Defaults to this channel.',\n required: false,\n },\n ],\n },\n {\n name: 'unsubscribe',\n description: 'Stop alerts for a subscription',\n usage: '/unsubscribe [subscription id]',\n options: [{ name: 'id', description: 'Subscription id from /status', required: false }],\n },\n { name: 'rules', description: 'List your rules', usage: '/rules' },\n {\n name: 'rule',\n description: 'Add, remove, enable or disable a rule',\n usage: '/rule add <json> | /rule rm <id> | /rule on <id> | /rule off <id>',\n options: [{ name: 'input', description: 'add <json> | rm <id> | on <id> | off <id>', required: true }],\n },\n {\n name: 'threshold',\n description: 'Set a rule minimum USD value',\n usage: '/threshold <rule id> <usd>',\n options: [\n { name: 'rule', description: 'Rule id', required: true },\n { name: 'usd', description: 'Minimum USD value', required: true },\n ],\n },\n {\n name: 'watch',\n description: 'Add a token to a rule watchlist',\n usage: '/watch <token address> [rule id]',\n options: [\n { name: 'token', description: 'Token contract address', required: true },\n { name: 'rule', description: 'Rule id (defaults to the first rule)', required: false },\n ],\n },\n {\n name: 'unwatch',\n description: 'Remove a token from a rule watchlist',\n usage: '/unwatch <token address> [rule id]',\n options: [\n { name: 'token', description: 'Token contract address', required: true },\n { name: 'rule', description: 'Rule id (defaults to the first rule)', required: false },\n ],\n },\n { name: 'tier', description: 'Show your tier and its limits', usage: '/tier' },\n { name: 'status', description: 'Show your subscriptions and delivery state', usage: '/status' },\n {\n name: 'link',\n description: 'Link a paying wallet (signature verified)',\n usage: '/link [address] [signature]',\n options: [\n { name: 'address', description: 'Wallet address that pays', required: false },\n { name: 'signature', description: 'Signature of the link message', required: false },\n ],\n },\n { name: 'upgrade', description: 'How to go premium', usage: '/upgrade' },\n]\n\nfunction ruleSummary(rule: Rule): string {\n const parts = [\n `${rule.enabled ? 'on ' : 'off'} ${rule.id}`,\n rule.name ? `\"${rule.name}\"` : null,\n `kinds=${rule.kinds.join('/')}`,\n `pads=${rule.launchpads.join('/')}`,\n rule.minUsd !== undefined ? `minUsd=${rule.minUsd}` : null,\n rule.maxUsd !== undefined ? `maxUsd=${rule.maxUsd}` : null,\n rule.minLiquidityUsd !== undefined ? `minLiq=${rule.minLiquidityUsd}` : null,\n rule.maxLiquidityUsd !== undefined ? `maxLiq=${rule.maxLiquidityUsd}` : null,\n rule.side !== 'any' ? `side=${rule.side}` : null,\n rule.tokens.length > 0 ? `tokens=${rule.tokens.length}` : null,\n rule.deployers.length > 0 ? `deployers=${rule.deployers.length}` : null,\n rule.rateLimit ? `rate=${JSON.stringify(rule.rateLimit)}` : null,\n `filters=${ruleComplexity(rule)}`,\n ].filter((part): part is string => part !== null)\n return parts.join(' ')\n}\n\n/** Build the command router. */\nexport function createCommandRouter(deps: CommandRouterDeps): CommandRouter {\n const now = deps.now ?? Date.now\n const newId = deps.newId ?? randomUUID\n\n function subscriptionsOf(context: CommandContext): Subscription[] {\n return deps.store.listSubscriptionsFor(context.subscriberId)\n }\n\n function firstSubscription(context: CommandContext): Subscription | null {\n return subscriptionsOf(context)[0] ?? null\n }\n\n async function handleSubscribe(context: CommandContext): Promise<CommandReply> {\n const target = context.args[0]?.trim() || context.defaultTarget\n if (!target) {\n return reply(\n 'I could not work out where to send alerts. Pass a target: /subscribe <discord webhook URL or channel id>',\n )\n }\n const existing = subscriptionsOf(context)\n if (existing.some((sub) => sub.target === target)) {\n return reply(`This target is already subscribed. Use /rules to edit it, or /unsubscribe to stop.`)\n }\n\n const decision = await deps.gate.checkAddSubscription(context.subscriberId, existing.length)\n if (!decision.allowed) return reply(`Cannot subscribe: ${decision.reason}`)\n\n const { policy } = await deps.gate.resolve(context.subscriberId)\n const rules = defaultRules().filter((rule) =>\n rule.kinds.every((kind) => policy.allowedKinds.includes(kind)),\n )\n const subscription = parseSubscription({\n id: newId(),\n subscriberId: context.subscriberId,\n platform: context.platform,\n target,\n enabled: true,\n rules: rules.slice(0, policy.maxRulesPerSubscription),\n createdAtMs: now(),\n })\n deps.store.saveSubscription(subscription)\n\n return html(\n [\n `<b>Subscribed.</b> Alerts will arrive here.`,\n `Subscription id: <code>${escapeHtml(subscription.id)}</code>`,\n `Tier: <b>${policy.tier}</b> (delivery delay ${policy.deliveryDelayMs / 1000}s, ${policy.maxAlertsPerHour} alerts/hour)`,\n '',\n 'Starter rules:',\n ...subscription.rules.map((rule) => `• <code>${escapeHtml(ruleSummary(rule))}</code>`),\n '',\n 'Edit them with /threshold, /watch and /rule. See /help.',\n ].join('\\n'),\n `Subscribed. id=${subscription.id} tier=${policy.tier}`,\n )\n }\n\n async function handleUnsubscribe(context: CommandContext): Promise<CommandReply> {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('You have no subscriptions. Use /subscribe to start one.')\n\n const id = context.args[0]?.trim()\n if (!id) {\n if (subs.length > 1) {\n return html(\n [\n 'You have several subscriptions. Name the one to remove:',\n ...subs.map((sub) => `• <code>${escapeHtml(sub.id)}</code> → ${escapeHtml(sub.target)}`),\n '',\n 'Usage: /unsubscribe &lt;subscription id&gt;',\n ].join('\\n'),\n 'Name the subscription to remove: ' + subs.map((sub) => sub.id).join(', '),\n )\n }\n const only = subs[0] as Subscription\n deps.store.deleteSubscription(only.id)\n return reply('Unsubscribed. No more alerts will be delivered here.')\n }\n\n const match = subs.find((sub) => sub.id === id)\n if (!match) return reply(`No subscription with id \"${id}\". Use /status to list yours.`)\n deps.store.deleteSubscription(match.id)\n return reply(`Unsubscribed ${match.id}.`)\n }\n\n function handleRules(context: CommandContext): CommandReply {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('You have no subscriptions yet. Use /subscribe to start one.')\n const lines: string[] = []\n for (const sub of subs) {\n lines.push(`<b>${escapeHtml(sub.target)}</b> (<code>${escapeHtml(sub.id)}</code>)`)\n if (sub.rules.length === 0) lines.push(' no rules: nothing will be delivered')\n for (const rule of sub.rules) lines.push(` • <code>${escapeHtml(ruleSummary(rule))}</code>`)\n }\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleRule(context: CommandContext): Promise<CommandReply> {\n const action = context.args[0]?.toLowerCase()\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe, then add rules.')\n\n if (action === 'add') {\n const json = context.rest.slice(context.rest.indexOf('add') + 3).trim()\n if (!json) {\n return html(\n [\n 'Usage: <code>/rule add {\"id\":\"big-whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":25000}</code>',\n '',\n 'Fields: id, name, enabled, kinds, launchpads, minUsd, maxUsd, minLiquidityUsd,',\n 'maxLiquidityUsd, tokens, deployers, excludeDeployers, side, reputation, rateLimit.',\n ].join('\\n'),\n 'Usage: /rule add {\"id\":\"big-whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":25000}',\n )\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch {\n return reply('That is not valid JSON. Example: /rule add {\"id\":\"whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":5000}')\n }\n const result = safeParseRule(parsed)\n if (!result.success) return reply(`Rule rejected: ${formatValidationError(result.error)}`)\n const rule = result.data\n\n if (subscription.rules.some((existing) => existing.id === rule.id)) {\n return reply(`A rule with id \"${rule.id}\" already exists. Remove it first with /rule rm ${rule.id}.`)\n }\n const decision = await deps.gate.checkAddRule(context.subscriberId, rule, subscription.rules.length)\n if (!decision.allowed) return reply(`Rule rejected: ${decision.reason}`)\n\n subscription.rules.push(rule)\n deps.store.saveSubscription(subscription)\n return html(\n `Rule added: <code>${escapeHtml(ruleSummary(rule))}</code>`,\n `Rule added: ${ruleSummary(rule)}`,\n )\n }\n\n if (action === 'rm' || action === 'remove' || action === 'delete') {\n const id = context.args[1]?.trim()\n if (!id) return reply('Usage: /rule rm <rule id>. List them with /rules.')\n const index = subscription.rules.findIndex((rule) => rule.id === id)\n if (index === -1) return reply(`No rule with id \"${id}\". List them with /rules.`)\n subscription.rules.splice(index, 1)\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${id} removed.`)\n }\n\n if (action === 'on' || action === 'off') {\n const id = context.args[1]?.trim()\n if (!id) return reply(`Usage: /rule ${action} <rule id>. List them with /rules.`)\n const rule = subscription.rules.find((entry) => entry.id === id)\n if (!rule) return reply(`No rule with id \"${id}\". List them with /rules.`)\n rule.enabled = action === 'on'\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${id} is now ${rule.enabled ? 'enabled' : 'disabled'}.`)\n }\n\n return html(\n [\n 'Usage:',\n '• <code>/rule add {json}</code>',\n '• <code>/rule rm &lt;id&gt;</code>',\n '• <code>/rule on &lt;id&gt;</code>',\n '• <code>/rule off &lt;id&gt;</code>',\n ].join('\\n'),\n 'Usage: /rule add {json} | /rule rm <id> | /rule on <id> | /rule off <id>',\n )\n }\n\n async function handleThreshold(context: CommandContext): Promise<CommandReply> {\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe.')\n const [ruleId, rawUsd] = context.args\n if (!ruleId || !rawUsd) {\n return reply('Usage: /threshold <rule id> <usd>. Example: /threshold whales 5000')\n }\n const rule = subscription.rules.find((entry) => entry.id === ruleId)\n if (!rule) return reply(`No rule with id \"${ruleId}\". List them with /rules.`)\n const usd = Number(rawUsd)\n if (!Number.isFinite(usd) || usd < 0) {\n return reply(`\"${rawUsd}\" is not a valid amount. Use a positive number, for example /threshold ${ruleId} 5000`)\n }\n if (rule.maxUsd !== undefined && usd > rule.maxUsd) {\n return reply(`minUsd ${usd} would exceed this rule's maxUsd ${rule.maxUsd}. Lower it or raise maxUsd first.`)\n }\n rule.minUsd = usd\n const decision = await deps.gate.checkRule(context.subscriberId, rule)\n if (!decision.allowed) return reply(`Change rejected: ${decision.reason}`)\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${ruleId} now requires at least $${usd} of value.`)\n }\n\n async function handleWatch(context: CommandContext, add: boolean): Promise<CommandReply> {\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe.')\n const rawToken = context.args[0]?.trim()\n if (!rawToken) {\n return reply(`Usage: /${add ? 'watch' : 'unwatch'} <token address> [rule id]`)\n }\n if (!/^0x[0-9a-fA-F]{40}$/.test(rawToken)) {\n return reply(`\"${rawToken}\" is not a token address. Pass the 0x contract address from the alert.`)\n }\n const token = getAddress(rawToken as `0x${string}`).toLowerCase()\n\n const ruleId = context.args[1]?.trim()\n const rule = ruleId\n ? subscription.rules.find((entry) => entry.id === ruleId)\n : subscription.rules[0]\n if (!rule) {\n return reply(\n ruleId ? `No rule with id \"${ruleId}\". List them with /rules.` : 'This subscription has no rules yet. Add one with /rule add.',\n )\n }\n\n if (add) {\n if (rule.tokens.includes(token)) return reply(`${rawToken} is already on rule ${rule.id}.`)\n rule.tokens.push(token)\n const decision = await deps.gate.checkRule(context.subscriberId, rule)\n if (!decision.allowed) {\n rule.tokens.pop()\n return reply(`Cannot add: ${decision.reason}`)\n }\n deps.store.saveSubscription(subscription)\n return reply(`Watching ${rawToken} on rule ${rule.id} (${rule.tokens.length} tokens).`)\n }\n\n const index = rule.tokens.indexOf(token)\n if (index === -1) return reply(`${rawToken} is not on rule ${rule.id}.`)\n rule.tokens.splice(index, 1)\n deps.store.saveSubscription(subscription)\n return reply(`Stopped watching ${rawToken} on rule ${rule.id}.`)\n }\n\n async function handleTier(context: CommandContext): Promise<CommandReply> {\n const { entitlement, policy } = await deps.gate.resolve(context.subscriberId)\n const expiry =\n entitlement.expiresAtMs === null\n ? 'no expiry'\n : `expires ${new Date(entitlement.expiresAtMs).toISOString()}`\n const lines = [\n `<b>Tier: ${escapeHtml(policy.tier)}</b> (${escapeHtml(expiry)}, source ${escapeHtml(entitlement.source)})`,\n `Subscriptions: up to ${policy.maxSubscriptions}`,\n `Rules per subscription: up to ${policy.maxRulesPerSubscription}`,\n `Filters per rule: up to ${policy.maxRuleComplexity}`,\n `Watchlist size: up to ${policy.maxWatchlistTokens}`,\n `Alerts per hour: up to ${policy.maxAlertsPerHour}`,\n `Delivery delay: ${policy.deliveryDelayMs / 1000}s`,\n `Liquidity and reputation filters: ${policy.allowOnChainLookupFilters ? 'included' : 'premium only'}`,\n `Event kinds: ${policy.allowedKinds.join(', ')}`,\n ]\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleStatus(context: CommandContext): Promise<CommandReply> {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('No subscriptions. Use /subscribe to start one.')\n const delivered = deps.store.countDeliveriesForSubscriber(context.subscriberId, now() - 3_600_000)\n const { policy } = await deps.gate.resolve(context.subscriberId)\n const lines = [\n `<b>${subs.length} subscription${subs.length === 1 ? '' : 's'}</b>, ${delivered} alerts in the last hour (cap ${policy.maxAlertsPerHour})`,\n ...subs.map(\n (sub) =>\n `• <code>${escapeHtml(sub.id)}</code> → ${escapeHtml(sub.target)} (${sub.rules.length} rules, ${sub.enabled ? 'enabled' : 'disabled'})`,\n ),\n ]\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleLink(context: CommandContext): Promise<CommandReply> {\n const [address, signature] = context.args\n if (!address) {\n const nonce = deps.store.issueLinkNonce(context.subscriberId, newId())\n const message = linkMessage(context.subscriberId, nonce)\n return html(\n [\n 'Sign this exact message with the wallet that pays, then send',\n `<code>/link &lt;address&gt; &lt;signature&gt;</code>`,\n '',\n `<pre>${escapeHtml(message)}</pre>`,\n ].join('\\n'),\n `Sign this message and reply with /link <address> <signature>:\\n${message}`,\n )\n }\n if (!signature) {\n return reply('Usage: /link <address> <signature>. Send /link on its own to get the message to sign.')\n }\n const nonce = deps.store.getLinkNonce(context.subscriberId)\n if (!nonce) {\n return reply('No pending link request. Send /link on its own first to get a fresh message to sign.')\n }\n const verified = await verifyWalletLink(context.subscriberId, nonce, address, signature)\n if (!verified) {\n return reply('That signature does not match. Sign the exact message from /link with the wallet you named.')\n }\n deps.store.linkWallet(context.subscriberId, verified)\n deps.store.clearLinkNonce(context.subscriberId)\n return reply(`Wallet ${verified} linked. Payments from it now count towards your subscription.`)\n }\n\n function handleUpgrade(): CommandReply {\n if (!deps.upgradeInstructions) {\n return reply(\n 'This deployment does not sell premium access. The operator grants it through configuration; ask them.',\n )\n }\n return html(escapeHtml(deps.upgradeInstructions), deps.upgradeInstructions)\n }\n\n function handleHelp(): CommandReply {\n const header = 'hood-alerts: Robinhood Chain memecoin alerts.'\n const kinds = 'Event kinds: launch, curve_trade, graduation, whale_trade.'\n const lifecycle =\n 'NOXA lists instantly (no bonding curve), so it never produces curve_trade or graduation. The Odyssey produces all four.'\n return html(\n [\n `<b>${escapeHtml(header)}</b>`,\n '',\n ...COMMANDS.map(\n (command) => `<code>${escapeHtml(command.usage)}</code> - ${escapeHtml(command.description)}`,\n ),\n '',\n escapeHtml(kinds),\n escapeHtml(lifecycle),\n ].join('\\n'),\n [\n header,\n '',\n ...COMMANDS.map((command) => `${command.usage} - ${command.description}`),\n '',\n kinds,\n lifecycle,\n ].join('\\n'),\n )\n }\n\n return {\n commands: COMMANDS,\n async handle(command: string, context: CommandContext): Promise<CommandReply> {\n const name = command.replace(/^\\//, '').split('@')[0]?.toLowerCase() ?? ''\n try {\n switch (name) {\n case 'start':\n case 'help':\n return handleHelp()\n case 'subscribe':\n return await handleSubscribe(context)\n case 'unsubscribe':\n return await handleUnsubscribe(context)\n case 'rules':\n return handleRules(context)\n case 'rule':\n return await handleRule(context)\n case 'threshold':\n return await handleThreshold(context)\n case 'watch':\n return await handleWatch(context, true)\n case 'unwatch':\n return await handleWatch(context, false)\n case 'tier':\n return await handleTier(context)\n case 'status':\n return await handleStatus(context)\n case 'link':\n return await handleLink(context)\n case 'upgrade':\n return handleUpgrade()\n default:\n return reply(`Unknown command \"${name}\". Send /help for the list.`)\n }\n } catch (error) {\n // A handler throwing must still answer the user. Silence looks like a\n // dead bot, which is worse than an error message.\n const message = error instanceof Error ? error.message : String(error)\n return reply(`That command failed: ${message}. Try /help, or report this if it keeps happening.`)\n }\n },\n }\n}\n","import { getAddress, verifyMessage, type Address } from 'viem'\nimport { USDG_DECIMALS, erc20Abi, type HoodClient } from 'hoodchain'\nimport { formatUnits } from 'viem'\nimport { fetchLogRange, type FetchLogRangeOptions } from '../events/logs.js'\nimport type { Tier } from './policy.js'\n\n/**\n * Entitlements: who is premium, and why.\n *\n * The service resolves a subscriber's tier through exactly one interface, so\n * there is a single place to audit and a single place to swap the commercial\n * model. Two implementations ship:\n *\n * 1. {@link createStaticEntitlementProvider} is the documented default. Premium\n * subscribers come from configuration (`PREMIUM_SUBSCRIBERS` in the\n * environment). It has no dependencies, works offline, and is what a\n * self-hoster running the bot for their own community wants.\n * 2. {@link createUsdgEntitlementProvider} is a working on-chain rail. It reads\n * USDG `Transfer` logs to the operator's receiving address from a wallet\n * the subscriber has linked, and accrues subscription time from those real\n * payments. It needs no facilitator, no card processor and no third-party\n * service: USDG is the chain's own dollar and the ledger is the chain.\n *\n * Wallet linking is signature-verified (EIP-191 `personal_sign` over a\n * server-issued nonce), so a subscriber cannot claim someone else's payments\n * by pasting their address.\n */\n\n/** A resolved entitlement. */\nexport interface Entitlement {\n subscriberId: string\n tier: Tier\n /** When premium lapses, ms since epoch. `null` for free or for a permanent grant. */\n expiresAtMs: number | null\n /** Which provider decided this, for support and for `/tier`. */\n source: string\n}\n\n/** The single entitlement chokepoint. */\nexport interface EntitlementProvider {\n /** Resolve a subscriber's tier. Must never throw: fall back to free. */\n get(subscriberId: string): Promise<Entitlement>\n}\n\nconst free = (subscriberId: string, source: string): Entitlement => ({\n subscriberId,\n tier: 'free',\n expiresAtMs: null,\n source,\n})\n\n/**\n * Configuration-driven entitlements: the documented default.\n *\n * @example\n * ```ts\n * const entitlements = createStaticEntitlementProvider({\n * premiumSubscribers: ['telegram:12345', 'discord:987654321'],\n * })\n * ```\n */\nexport function createStaticEntitlementProvider(options: {\n premiumSubscribers?: readonly string[]\n /** Treat everyone as premium. For a private self-hosted deployment. */\n allPremium?: boolean\n}): EntitlementProvider {\n const premium = new Set((options.premiumSubscribers ?? []).map((id) => id.trim()).filter(Boolean))\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n if (options.allPremium === true || premium.has(subscriberId)) {\n return { subscriberId, tier: 'premium', expiresAtMs: null, source: 'static' }\n }\n return free(subscriberId, 'static')\n },\n }\n}\n\n/** Lookup of the wallet a subscriber has verifiably linked. */\nexport interface WalletLinkStore {\n /** The linked, signature-verified wallet for a subscriber, or `null`. */\n walletOf(subscriberId: string): Promise<Address | null>\n}\n\n/** Options for {@link createUsdgEntitlementProvider}. */\nexport interface UsdgEntitlementOptions {\n client: HoodClient\n /** The operator's receiving address. Payments to any other address are ignored. */\n receiver: Address\n /** Price of one subscription period, in whole USDG. */\n pricePerPeriodUsdg: number\n /** Length of a period in days. @defaultValue `30` */\n periodDays?: number\n /** Where verified wallet links come from. */\n links: WalletLinkStore\n /** First block to scan for payments. @defaultValue `0n` */\n fromBlock?: bigint\n /** Cache lifetime per subscriber, in ms. @defaultValue `300_000` (5 minutes) */\n ttlMs?: number\n /** Retry/bisect options for the payment scan. */\n logOptions?: FetchLogRangeOptions\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\nconst transferEvent = {\n type: 'event',\n name: 'Transfer',\n inputs: [\n { name: 'from', type: 'address', indexed: true },\n { name: 'to', type: 'address', indexed: true },\n { name: 'value', type: 'uint256', indexed: false },\n ],\n} as const\n\n/**\n * Accrue an expiry from a payment history. Each payment buys\n * `floor(amount / price)` periods, starting from whenever the current\n * entitlement would have lapsed, so renewing early extends rather than\n * overwrites.\n *\n * Exported because it is the part worth testing on its own: everything else in\n * the provider is a log query.\n */\nexport function accrueExpiry(\n payments: readonly { atMs: number; amountUsdg: number }[],\n pricePerPeriodUsdg: number,\n periodDays: number,\n): number | null {\n if (pricePerPeriodUsdg <= 0) return null\n const periodMs = periodDays * 86_400_000\n const ordered = [...payments].sort((a, b) => a.atMs - b.atMs)\n let expiry: number | null = null\n for (const payment of ordered) {\n const periods = Math.floor(payment.amountUsdg / pricePerPeriodUsdg)\n if (periods < 1) continue\n const start: number = expiry !== null && expiry > payment.atMs ? expiry : payment.atMs\n expiry = start + periods * periodMs\n }\n return expiry\n}\n\n/**\n * On-chain USDG entitlements.\n *\n * @example\n * ```ts\n * const entitlements = createUsdgEntitlementProvider({\n * client: hood,\n * receiver: '0xYourReceivingAddress',\n * pricePerPeriodUsdg: 25,\n * links: store, // the service's SQLite store implements WalletLinkStore\n * })\n * const entitlement = await entitlements.get('telegram:12345')\n * ```\n */\nexport function createUsdgEntitlementProvider(options: UsdgEntitlementOptions): EntitlementProvider {\n const ttlMs = options.ttlMs ?? 300_000\n const now = options.now ?? Date.now\n const periodDays = options.periodDays ?? 30\n const usdg =\n options.client.network === 'testnet'\n ? '0x7E955252E15c84f5768B83c41a71F9eba181802F'\n : '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168'\n const cache = new Map<string, { entitlement: Entitlement; at: number }>()\n\n async function resolve(subscriberId: string): Promise<Entitlement> {\n const wallet = await options.links.walletOf(subscriberId)\n if (!wallet) return free(subscriberId, 'usdg:no-linked-wallet')\n\n const head = await options.client.public.getBlockNumber()\n const logs = await fetchLogRange(\n (from, to) =>\n options.client.public.getLogs({\n address: usdg as Address,\n event: transferEvent,\n args: { from: wallet, to: getAddress(options.receiver) },\n fromBlock: from,\n toBlock: to,\n }),\n options.fromBlock ?? 0n,\n head,\n options.logOptions ?? {},\n )\n if (logs.length === 0) return free(subscriberId, 'usdg:no-payments')\n\n const payments: { atMs: number; amountUsdg: number }[] = []\n for (const log of logs) {\n const value = log.args.value as bigint | undefined\n if (value === undefined || value === 0n) continue\n const block = await options.client.public.getBlock({\n blockNumber: log.blockNumber,\n includeTransactions: false,\n })\n payments.push({\n atMs: Number(block.timestamp) * 1000,\n amountUsdg: Number(formatUnits(value, USDG_DECIMALS)),\n })\n }\n\n const expiresAtMs = accrueExpiry(payments, options.pricePerPeriodUsdg, periodDays)\n if (expiresAtMs === null || expiresAtMs <= now()) {\n return { subscriberId, tier: 'free', expiresAtMs, source: 'usdg:lapsed' }\n }\n return { subscriberId, tier: 'premium', expiresAtMs, source: 'usdg' }\n }\n\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n const hit = cache.get(subscriberId)\n if (hit && now() - hit.at < ttlMs) return hit.entitlement\n try {\n const entitlement = await resolve(subscriberId)\n cache.set(subscriberId, { entitlement, at: now() })\n return entitlement\n } catch (error) {\n // An RPC failure must never silently downgrade a paying subscriber to\n // free with no explanation, and must never upgrade a free one. Serve\n // the last known answer if there is one, otherwise free with a source\n // that says exactly what happened.\n if (hit) return hit.entitlement\n const message = error instanceof Error ? error.message : String(error)\n return free(subscriberId, `usdg:unavailable (${message})`)\n }\n },\n }\n}\n\n/**\n * Chain several providers: the first one that returns premium wins. Lets an\n * operator grant comps by configuration while still selling on chain.\n */\nexport function chainEntitlementProviders(\n ...providers: readonly EntitlementProvider[]\n): EntitlementProvider {\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n let last: Entitlement = free(subscriberId, 'chain:empty')\n for (const provider of providers) {\n const entitlement = await provider.get(subscriberId)\n if (entitlement.tier === 'premium') return entitlement\n last = entitlement\n }\n return last\n },\n }\n}\n\n/** The message a subscriber signs to link a wallet. */\nexport function linkMessage(subscriberId: string, nonce: string): string {\n return `hood-alerts wallet link\\nsubscriber: ${subscriberId}\\nnonce: ${nonce}`\n}\n\n/**\n * Verify a wallet link signature (EIP-191 `personal_sign`).\n *\n * The subscriber signs {@link linkMessage} with the wallet that pays, and the\n * service checks the signature before storing the link. Without this step,\n * anyone could paste a paying subscriber's address and inherit their\n * entitlement.\n *\n * @returns the checksummed address on success, `null` on any failure (bad\n * signature shape, wrong signer, malformed address).\n *\n * @example\n * ```ts\n * const nonce = crypto.randomUUID()\n * // the user signs linkMessage('telegram:12345', nonce) in their wallet\n * const address = await verifyWalletLink('telegram:12345', nonce, wallet, signature)\n * if (address) await store.linkWallet('telegram:12345', address)\n * ```\n */\nexport async function verifyWalletLink(\n subscriberId: string,\n nonce: string,\n address: string,\n signature: string,\n): Promise<Address | null> {\n if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) return null\n if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return null\n try {\n const valid = await verifyMessage({\n address: getAddress(address),\n message: linkMessage(subscriberId, nonce),\n signature: signature as `0x${string}`,\n })\n return valid ? getAddress(address) : null\n } catch {\n return null\n }\n}\n","/** Structured logging: one JSON object per line, machine-readable by default. */\n\n/** Log levels, ordered. */\nexport const LEVELS = ['debug', 'info', 'warn', 'error'] as const\n\n/** A log level. */\nexport type LogLevel = (typeof LEVELS)[number]\n\n/** Structured fields attached to a log line. */\nexport type LogFields = Record<string, unknown>\n\n/** The logger the service uses everywhere. */\nexport interface Logger {\n debug(message: string, fields?: LogFields): void\n info(message: string, fields?: LogFields): void\n warn(message: string, fields?: LogFields): void\n error(message: string, fields?: LogFields): void\n /** A logger that adds `fields` to every line, for per-component context. */\n child(fields: LogFields): Logger\n}\n\n/** Options for {@link createLogger}. */\nexport interface LoggerOptions {\n /** Minimum level emitted. @defaultValue `'info'` */\n level?: LogLevel\n /** Where lines go. @defaultValue `console.log` */\n write?: (line: string) => void\n /** Fields added to every line. */\n base?: LogFields\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\n/**\n * Serialize a value that may contain a bigint or an Error. Log lines must\n * never throw: a `TypeError: Do not know how to serialize a BigInt` inside an\n * error handler loses the error it was reporting.\n */\nfunction safeStringify(value: unknown): string {\n return JSON.stringify(value, (_key, raw: unknown) => {\n if (typeof raw === 'bigint') return raw.toString()\n if (raw instanceof Error) return { name: raw.name, message: raw.message, stack: raw.stack }\n return raw\n })\n}\n\n/** Build a structured logger. */\nexport function createLogger(options: LoggerOptions = {}): Logger {\n const minimum = LEVELS.indexOf(options.level ?? 'info')\n const write = options.write ?? ((line: string): void => console.log(line))\n const now = options.now ?? Date.now\n const base = options.base ?? {}\n\n const emit = (level: LogLevel, message: string, fields?: LogFields): void => {\n if (LEVELS.indexOf(level) < minimum) return\n write(\n safeStringify({\n time: new Date(now()).toISOString(),\n level,\n message,\n ...base,\n ...(fields ?? {}),\n }),\n )\n }\n\n return {\n debug: (message, fields) => emit('debug', message, fields),\n info: (message, fields) => emit('info', message, fields),\n warn: (message, fields) => emit('warn', message, fields),\n error: (message, fields) => emit('error', message, fields),\n child(fields: LogFields): Logger {\n return createLogger({ ...options, base: { ...base, ...fields } })\n },\n }\n}\n\n/** A logger that discards everything. For tests and for `--quiet`. */\nexport function createSilentLogger(): Logger {\n const silent: Logger = {\n debug: () => undefined,\n info: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n child: () => silent,\n }\n return silent\n}\n","import { createTelegramClient, type TelegramClient, type TelegramClientOptions } from '../notifiers/telegram.js'\nimport type { Logger } from '../service/logger.js'\nimport { createSilentLogger } from '../service/logger.js'\nimport type { CommandContext, CommandRouter } from './commands.js'\n\n/**\n * The Telegram bot: long polling, because it needs no public URL, no TLS\n * certificate and no inbound firewall rule. A self-hoster runs the container\n * and the bot works; a webhook deployment would need all three before the\n * first `/start`.\n *\n * `getUpdates` is called with a long timeout, so the loop is idle rather than\n * busy between messages, and the offset is advanced past every update the loop\n * has handled so a restart never re-processes an old command.\n */\n\n/** Options for {@link createTelegramBot}. */\nexport interface TelegramBotOptions extends TelegramClientOptions {\n router: CommandRouter\n logger?: Logger\n /** Reuse the notifier's client so both share one rate-limit path. */\n client?: TelegramClient\n /** Long-poll timeout in seconds. @defaultValue `30` */\n pollTimeoutSeconds?: number\n /** Persist the update offset so a restart resumes cleanly. */\n offsetStore?: { get(): number | null; set(offset: number): void }\n}\n\n/** A running bot. */\nexport interface TelegramBot {\n /** Poll once and handle whatever arrived. Returns how many updates it handled. */\n pollOnce(): Promise<number>\n /** Publish the command list so Telegram shows the `/` menu. */\n publishCommands(): Promise<boolean>\n /** Start the loop. Resolves when {@link TelegramBot.stop} is called. */\n start(): Promise<void>\n /** Stop the loop after the current poll. */\n stop(): void\n}\n\ninterface TelegramUpdate {\n update_id: number\n message?: {\n message_id: number\n text?: string\n chat?: { id: number }\n from?: { id: number; is_bot?: boolean }\n }\n}\n\nfunction parseCommand(text: string): { command: string; args: string[]; rest: string } | null {\n const trimmed = text.trim()\n if (!trimmed.startsWith('/')) return null\n const [head, ...tail] = trimmed.split(/\\s+/)\n if (!head) return null\n return { command: head, args: tail, rest: trimmed.slice(head.length).trim() }\n}\n\n/**\n * Build the Telegram bot.\n *\n * @example\n * ```ts\n * const bot = createTelegramBot({ botToken: process.env.TELEGRAM_BOT_TOKEN!, router })\n * await bot.publishCommands()\n * await bot.start()\n * ```\n */\nexport function createTelegramBot(options: TelegramBotOptions): TelegramBot {\n const client = options.client ?? createTelegramClient(options)\n const logger = options.logger ?? createSilentLogger()\n const pollTimeout = options.pollTimeoutSeconds ?? 30\n let offset = options.offsetStore?.get() ?? 0\n let running = false\n\n async function handleUpdate(update: TelegramUpdate): Promise<void> {\n const message = update.message\n const text = message?.text\n const chatId = message?.chat?.id\n const fromId = message?.from?.id\n if (!text || chatId === undefined || fromId === undefined || message?.from?.is_bot === true) return\n\n const parsed = parseCommand(text)\n if (!parsed) return\n\n const context: CommandContext = {\n subscriberId: `telegram:${fromId}`,\n platform: 'telegram',\n defaultTarget: String(chatId),\n args: parsed.args,\n rest: parsed.rest,\n }\n const reply = await options.router.handle(parsed.command, context)\n const response = await client.call('sendMessage', {\n chat_id: chatId,\n text: reply.html,\n parse_mode: 'HTML',\n link_preview_options: { is_disabled: true },\n })\n if (!response.ok) {\n logger.warn('telegram reply failed', {\n chatId,\n command: parsed.command,\n error: response.description,\n })\n }\n }\n\n return {\n async publishCommands(): Promise<boolean> {\n const response = await client.call('setMyCommands', {\n commands: options.router.commands.map((command) => ({\n command: command.name,\n description: command.description.slice(0, 256),\n })),\n })\n if (!response.ok) logger.warn('setMyCommands failed', { error: response.description })\n return response.ok\n },\n\n async pollOnce(): Promise<number> {\n const response = await client.call('getUpdates', {\n ...(offset > 0 ? { offset } : {}),\n timeout: pollTimeout,\n allowed_updates: ['message'],\n })\n if (!response.ok) {\n logger.warn('getUpdates failed', { error: response.description })\n return 0\n }\n const updates = Array.isArray(response.result) ? (response.result as TelegramUpdate[]) : []\n for (const update of updates) {\n try {\n await handleUpdate(update)\n } catch (error) {\n // One bad command must not stall the loop or replay forever: log it\n // and still advance past it.\n logger.error('telegram update handler threw', {\n updateId: update.update_id,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n offset = update.update_id + 1\n options.offsetStore?.set(offset)\n }\n return updates.length\n },\n\n async start(): Promise<void> {\n running = true\n logger.info('telegram bot started', { offset })\n while (running) {\n try {\n await this.pollOnce()\n } catch (error) {\n logger.error('telegram poll failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n await new Promise((resolve) => setTimeout(resolve, 2_000))\n }\n }\n logger.info('telegram bot stopped')\n },\n\n stop(): void {\n running = false\n },\n }\n}\n","import { createPublicKey, verify as cryptoVerify } from 'node:crypto'\nimport { resolveFetch } from '../notifiers/http.js'\nimport type { FetchLike } from '../notifiers/types.js'\nimport { truncate } from '../notifiers/escape.js'\nimport type { CommandContext, CommandRouter, CommandSpec } from './commands.js'\n\n/**\n * The Discord bot: slash commands over the HTTP interactions endpoint.\n *\n * Discord offers two ways to receive commands: a persistent gateway\n * WebSocket, or an HTTPS interactions endpoint. This package uses the\n * endpoint, because the service already runs an HTTP server for `/health` and\n * `/metrics`, and an endpoint scales to zero and survives a restart with no\n * reconnect logic. The price is that every request must be signature-verified,\n * which Discord enforces: it sends deliberately invalid signatures during\n * setup and refuses to save an endpoint that answers them with anything other\n * than `401`.\n *\n * Verification is Ed25519 over `timestamp + rawBody`, using the application's\n * public key. Node's `crypto` verifies Ed25519 natively, so this needs no\n * dependency: the raw 32-byte key is wrapped in the fixed SPKI DER prefix for\n * `id-Ed25519` (RFC 8410) and handed to `createPublicKey`.\n */\n\n/** Discord's interaction type numbers, for the two kinds handled here. */\nexport const INTERACTION_TYPE = { PING: 1, APPLICATION_COMMAND: 2 } as const\n\n/** Discord's interaction callback types. */\nexport const CALLBACK_TYPE = { PONG: 1, CHANNEL_MESSAGE_WITH_SOURCE: 4 } as const\n\n/** Discord marks a message ephemeral (only the caller sees it) with this flag. */\nexport const EPHEMERAL_FLAG = 1 << 6\n\n/** RFC 8410 SPKI prefix for an Ed25519 public key. */\nconst ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')\n\n/**\n * Verify a Discord interaction signature.\n *\n * @param publicKey the application's hex public key from the developer portal.\n * @returns `true` only for a signature that verifies. Any malformed input is\n * `false`, never a throw: a crash here would answer Discord with a 500 and\n * fail its endpoint validation just as surely as a wrong answer.\n */\nexport function verifyDiscordSignature(\n publicKey: string,\n signature: string,\n timestamp: string,\n rawBody: string,\n): boolean {\n if (!/^[0-9a-fA-F]{64}$/.test(publicKey)) return false\n if (!/^[0-9a-fA-F]{128}$/.test(signature)) return false\n if (!timestamp) return false\n try {\n const key = createPublicKey({\n key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(publicKey, 'hex')]),\n format: 'der',\n type: 'spki',\n })\n return cryptoVerify(\n null,\n Buffer.from(timestamp + rawBody, 'utf8'),\n key,\n Buffer.from(signature, 'hex'),\n )\n } catch {\n return false\n }\n}\n\n/** A Discord slash-command definition, as the API expects it. */\nexport interface DiscordCommandDefinition {\n name: string\n description: string\n /** Option type 3 is STRING, the only type these commands need. */\n options?: { type: 3; name: string; description: string; required: boolean }[]\n}\n\n/** Convert the shared command catalogue into Discord slash-command definitions. */\nexport function discordCommandDefinitions(\n commands: readonly CommandSpec[],\n): DiscordCommandDefinition[] {\n return commands.map((command) => ({\n name: command.name,\n // Discord caps descriptions at 100 characters and rejects the whole\n // registration if any one is longer.\n description: truncate(command.description, 100),\n ...(command.options && command.options.length > 0\n ? {\n options: command.options.map((option) => ({\n type: 3 as const,\n name: option.name,\n description: truncate(option.description, 100),\n required: option.required,\n })),\n }\n : {}),\n }))\n}\n\n/** Options for {@link registerDiscordCommands}. */\nexport interface RegisterCommandsOptions {\n applicationId: string\n botToken: string\n /** Register to one guild (instant) instead of globally (up to an hour to propagate). */\n guildId?: string\n apiBase?: string\n fetch?: FetchLike\n}\n\n/**\n * Register slash commands with Discord.\n *\n * @returns the number of commands Discord accepted.\n * @throws when Discord rejects the registration, with its own error message\n * attached: a silent failure here means a bot that appears installed and has\n * no commands.\n *\n * @example\n * ```bash\n * DISCORD_APPLICATION_ID=... DISCORD_BOT_TOKEN=... npm run register:discord\n * ```\n */\nexport async function registerDiscordCommands(\n commands: readonly CommandSpec[],\n options: RegisterCommandsOptions,\n): Promise<number> {\n const fetchImpl = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://discord.com/api/v10').replace(/\\/+$/, '')\n const url = options.guildId\n ? `${apiBase}/applications/${options.applicationId}/guilds/${options.guildId}/commands`\n : `${apiBase}/applications/${options.applicationId}/commands`\n\n const response = await fetchImpl(url, {\n method: 'PUT',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bot ${options.botToken}`,\n },\n body: JSON.stringify(discordCommandDefinitions(commands)),\n })\n const raw = await response.text()\n if (!response.ok) {\n throw new Error(`hood-alerts: Discord rejected the command registration (${response.status}): ${raw}`)\n }\n try {\n const parsed: unknown = JSON.parse(raw)\n return Array.isArray(parsed) ? parsed.length : 0\n } catch {\n return 0\n }\n}\n\n/** The shape of an incoming interaction, narrowed to what the router needs. */\ninterface Interaction {\n type: number\n guild_id?: string\n channel_id?: string\n member?: { user?: { id?: string } }\n user?: { id?: string }\n data?: {\n name?: string\n options?: { name: string; value?: unknown }[]\n }\n}\n\n/** What to answer Discord with. */\nexport interface InteractionResponse {\n status: number\n body: unknown\n}\n\n/** Options for {@link createDiscordInteractionHandler}. */\nexport interface DiscordInteractionOptions {\n router: CommandRouter\n /** The application's public key, for signature verification. */\n publicKey: string\n /** Reply only to the caller. @defaultValue `true` */\n ephemeral?: boolean\n}\n\n/**\n * Build the interactions handler. Mount it on the service's HTTP server at\n * whatever path is configured as the application's Interactions Endpoint URL.\n *\n * @example\n * ```ts\n * const interactions = createDiscordInteractionHandler({ router, publicKey })\n * // inside the HTTP server:\n * const answer = await interactions.handle({ signature, timestamp, rawBody })\n * res.writeHead(answer.status, { 'content-type': 'application/json' })\n * res.end(JSON.stringify(answer.body))\n * ```\n */\nexport function createDiscordInteractionHandler(options: DiscordInteractionOptions): {\n handle(request: { signature: string | null; timestamp: string | null; rawBody: string }): Promise<InteractionResponse>\n} {\n const ephemeral = options.ephemeral ?? true\n\n return {\n async handle(request): Promise<InteractionResponse> {\n if (\n !request.signature ||\n !request.timestamp ||\n !verifyDiscordSignature(options.publicKey, request.signature, request.timestamp, request.rawBody)\n ) {\n return { status: 401, body: { error: 'invalid request signature' } }\n }\n\n let interaction: Interaction\n try {\n interaction = JSON.parse(request.rawBody) as Interaction\n } catch {\n return { status: 400, body: { error: 'body is not JSON' } }\n }\n\n if (interaction.type === INTERACTION_TYPE.PING) {\n return { status: 200, body: { type: CALLBACK_TYPE.PONG } }\n }\n if (interaction.type !== INTERACTION_TYPE.APPLICATION_COMMAND) {\n return { status: 400, body: { error: `unsupported interaction type ${interaction.type}` } }\n }\n\n const name = interaction.data?.name\n if (!name) return { status: 400, body: { error: 'interaction carried no command name' } }\n\n const values = (interaction.data?.options ?? [])\n .map((option) => (option.value === undefined ? '' : String(option.value)))\n .filter((value) => value.length > 0)\n // A Discord option value can itself contain spaces (a JSON rule body, a\n // webhook URL), so the router receives the option values as separate\n // args and the joined string as `rest`.\n const rest = values.join(' ')\n const args = values.length === 1 ? (values[0] as string).split(/\\s+/) : values\n\n const userId = interaction.member?.user?.id ?? interaction.user?.id ?? ''\n const context: CommandContext = {\n // A guild pays once for all its channels; a DM is billed to the user.\n subscriberId: `discord:${interaction.guild_id ?? userId}`,\n platform: 'discord',\n defaultTarget: interaction.channel_id ?? '',\n args,\n rest: `${name} ${rest}`.trim(),\n }\n\n const reply = await options.router.handle(name, context)\n return {\n status: 200,\n body: {\n type: CALLBACK_TYPE.CHANNEL_MESSAGE_WITH_SOURCE,\n data: {\n // Discord hard-caps message content at 2000 characters.\n content: truncate(reply.text, 1_900),\n ...(ephemeral ? { flags: EPHEMERAL_FLAG } : {}),\n },\n },\n }\n },\n }\n}\n","import type { AlertEventKind } from '../events/types.js'\nimport { ruleComplexity, usesOnChainLookups, type Rule } from '../rules/schema.js'\n\n/**\n * Tiering, as an enforced policy layer rather than a marketing page.\n *\n * Every limit here exists because it maps to a real cost the service pays:\n *\n * - `maxSubscriptions` / `maxRulesPerSubscription` bound the per-event fan-out.\n * - `maxRuleComplexity` bounds how much work one rule can ask for.\n * - `allowOnChainLookupFilters` gates the filters that cost an RPC read per\n * candidate event (liquidity, deployer reputation). This is the expensive\n * one, so it is the clearest free/premium line.\n * - `maxAlertsPerHour` bounds delivery throughput.\n * - `deliveryDelayMs` is the latency difference. Free alerts are held briefly;\n * premium alerts go out as soon as the block is confirmed. It is enforced in\n * the outbox (the delivery row carries `not_before`), so it survives a\n * restart instead of being a `setTimeout` that a crash erases.\n * - `allowedKinds` keeps the highest-volume stream (every bonding-curve trade)\n * on the paid tier, because it is a firehose, not an alert.\n */\n\n/** The two tiers. */\nexport type Tier = 'free' | 'premium'\n\n/** What a tier is allowed to do. */\nexport interface TierPolicy {\n tier: Tier\n /** Max delivery targets one subscriber may have. */\n maxSubscriptions: number\n /** Max rules per subscription. */\n maxRulesPerSubscription: number\n /** Max {@link ruleComplexity} score for a single rule. */\n maxRuleComplexity: number\n /** Max entries in a rule's token or deployer watchlist. */\n maxWatchlistTokens: number\n /** Max alerts delivered per subscriber per rolling hour. */\n maxAlertsPerHour: number\n /** How long alerts are held before delivery, in ms. */\n deliveryDelayMs: number\n /** May rules use liquidity and reputation filters (each costs an RPC read). */\n allowOnChainLookupFilters: boolean\n /** Event kinds this tier may subscribe to. */\n allowedKinds: readonly AlertEventKind[]\n}\n\n/** The shipped policy table. Override per deployment through the service config. */\nexport const TIER_POLICIES: Record<Tier, TierPolicy> = {\n free: {\n tier: 'free',\n maxSubscriptions: 1,\n maxRulesPerSubscription: 3,\n maxRuleComplexity: 3,\n maxWatchlistTokens: 5,\n maxAlertsPerHour: 30,\n deliveryDelayMs: 60_000,\n allowOnChainLookupFilters: false,\n allowedKinds: ['launch', 'graduation', 'whale_trade'],\n },\n premium: {\n tier: 'premium',\n maxSubscriptions: 10,\n maxRulesPerSubscription: 50,\n maxRuleComplexity: 24,\n maxWatchlistTokens: 500,\n maxAlertsPerHour: 2_000,\n deliveryDelayMs: 0,\n allowOnChainLookupFilters: true,\n allowedKinds: ['launch', 'curve_trade', 'graduation', 'whale_trade'],\n },\n}\n\n/** A policy decision. `allowed: false` always carries a user-facing reason. */\nexport interface PolicyDecision {\n allowed: boolean\n /** Present when `allowed` is false. Written to be shown to the subscriber. */\n reason: string | null\n}\n\nconst OK: PolicyDecision = { allowed: true, reason: null }\nconst deny = (reason: string): PolicyDecision => ({ allowed: false, reason })\n\n/** May this subscriber add another subscription? */\nexport function canAddSubscription(policy: TierPolicy, currentCount: number): PolicyDecision {\n if (currentCount >= policy.maxSubscriptions) {\n return deny(\n `the ${policy.tier} tier allows ${policy.maxSubscriptions} subscription${\n policy.maxSubscriptions === 1 ? '' : 's'\n } and you have ${currentCount}. Upgrade with /upgrade, or remove one with /unsubscribe.`,\n )\n }\n return OK\n}\n\n/** May this rule be added to a subscription that already has `currentRuleCount` rules? */\nexport function canAddRule(\n policy: TierPolicy,\n rule: Rule,\n currentRuleCount: number,\n): PolicyDecision {\n if (currentRuleCount >= policy.maxRulesPerSubscription) {\n return deny(\n `the ${policy.tier} tier allows ${policy.maxRulesPerSubscription} rules per subscription and you have ${currentRuleCount}.`,\n )\n }\n return isRuleAllowed(policy, rule)\n}\n\n/** Is this rule within the tier's limits, regardless of how many exist? */\nexport function isRuleAllowed(policy: TierPolicy, rule: Rule): PolicyDecision {\n const disallowedKinds = rule.kinds.filter((kind) => !policy.allowedKinds.includes(kind))\n if (disallowedKinds.length > 0) {\n return deny(\n `the ${policy.tier} tier does not include ${disallowedKinds.join(', ')} alerts. Allowed: ${policy.allowedKinds.join(', ')}.`,\n )\n }\n const complexity = ruleComplexity(rule)\n if (complexity > policy.maxRuleComplexity) {\n return deny(\n `this rule uses ${complexity} filters and the ${policy.tier} tier allows ${policy.maxRuleComplexity}.`,\n )\n }\n if (rule.tokens.length > policy.maxWatchlistTokens) {\n return deny(\n `the ${policy.tier} tier allows ${policy.maxWatchlistTokens} watchlist tokens and this rule has ${rule.tokens.length}.`,\n )\n }\n if (rule.deployers.length + rule.excludeDeployers.length > policy.maxWatchlistTokens) {\n return deny(\n `the ${policy.tier} tier allows ${policy.maxWatchlistTokens} watched deployers per rule.`,\n )\n }\n if (!policy.allowOnChainLookupFilters && usesOnChainLookups(rule)) {\n return deny(\n `liquidity and deployer-reputation filters are a premium feature: each one costs an on-chain read per candidate event.`,\n )\n }\n return OK\n}\n\n/** Resolve the policy for a tier. */\nexport function policyFor(tier: Tier, overrides?: Partial<Record<Tier, Partial<TierPolicy>>>): TierPolicy {\n const base = TIER_POLICIES[tier]\n const override = overrides?.[tier]\n return override ? { ...base, ...override, tier } : base\n}\n","import type { Rule } from '../rules/schema.js'\nimport type { Entitlement, EntitlementProvider } from './entitlements.js'\nimport {\n canAddRule,\n canAddSubscription,\n isRuleAllowed,\n policyFor,\n type PolicyDecision,\n type Tier,\n type TierPolicy,\n} from './policy.js'\n\n/**\n * The entitlement chokepoint.\n *\n * Every tier question in the codebase goes through this object: the bot asks\n * it before accepting a subscription or a rule, and the dispatcher asks it\n * before queuing a delivery. Nothing else reads {@link TIER_POLICIES}\n * directly. One place to audit, one place to change the commercial model, no\n * chance of the bot and the dispatcher disagreeing about who is premium.\n */\nexport interface EntitlementGate {\n /** Resolve tier, policy and expiry for a subscriber. */\n resolve(subscriberId: string): Promise<{ entitlement: Entitlement; policy: TierPolicy }>\n /** May they add another subscription? */\n checkAddSubscription(subscriberId: string, currentCount: number): Promise<PolicyDecision>\n /** May they add this rule to a subscription with `currentRuleCount` rules? */\n checkAddRule(subscriberId: string, rule: Rule, currentRuleCount: number): Promise<PolicyDecision>\n /** Is this rule within their tier, ignoring counts (used when editing)? */\n checkRule(subscriberId: string, rule: Rule): Promise<PolicyDecision>\n /** How long a delivery for this subscriber is held before sending, in ms. */\n deliveryDelayMs(subscriberId: string): Promise<number>\n /** Have they exhausted their hourly delivery budget? */\n checkThroughput(subscriberId: string, deliveredInLastHour: number): Promise<PolicyDecision>\n}\n\n/** Options for {@link createEntitlementGate}. */\nexport interface EntitlementGateOptions {\n provider: EntitlementProvider\n /** Per-deployment overrides of the shipped tier table. */\n policyOverrides?: Partial<Record<Tier, Partial<TierPolicy>>>\n}\n\n/** Build the gate. */\nexport function createEntitlementGate(options: EntitlementGateOptions): EntitlementGate {\n async function resolve(\n subscriberId: string,\n ): Promise<{ entitlement: Entitlement; policy: TierPolicy }> {\n const entitlement = await options.provider.get(subscriberId)\n return { entitlement, policy: policyFor(entitlement.tier, options.policyOverrides) }\n }\n\n return {\n resolve,\n async checkAddSubscription(subscriberId: string, currentCount: number): Promise<PolicyDecision> {\n const { policy } = await resolve(subscriberId)\n return canAddSubscription(policy, currentCount)\n },\n async checkAddRule(\n subscriberId: string,\n rule: Rule,\n currentRuleCount: number,\n ): Promise<PolicyDecision> {\n const { policy } = await resolve(subscriberId)\n return canAddRule(policy, rule, currentRuleCount)\n },\n async checkRule(subscriberId: string, rule: Rule): Promise<PolicyDecision> {\n const { policy } = await resolve(subscriberId)\n return isRuleAllowed(policy, rule)\n },\n async deliveryDelayMs(subscriberId: string): Promise<number> {\n const { policy } = await resolve(subscriberId)\n return policy.deliveryDelayMs\n },\n async checkThroughput(\n subscriberId: string,\n deliveredInLastHour: number,\n ): Promise<PolicyDecision> {\n const { policy } = await resolve(subscriberId)\n if (deliveredInLastHour >= policy.maxAlertsPerHour) {\n return {\n allowed: false,\n reason: `hourly alert cap reached (${policy.maxAlertsPerHour} on the ${policy.tier} tier)`,\n }\n }\n return { allowed: true, reason: null }\n },\n }\n}\n","import { z } from 'zod'\nimport type { LogLevel } from './logger.js'\n\n/**\n * Service configuration, validated at startup.\n *\n * Every value is checked before anything connects: a malformed\n * `WHALE_MIN_USD` should fail in the first second with a clear message, not\n * three hours later as a `NaN` comparison that silently matched every swap.\n */\n\nconst bool = (defaultValue: boolean) =>\n z\n .string()\n .optional()\n .transform((value) => {\n if (value === undefined || value.trim() === '') return defaultValue\n return /^(1|true|yes|on)$/i.test(value.trim())\n })\n\nconst int = (defaultValue: number, min: number, max: number) =>\n z\n .string()\n .optional()\n .transform((value) => (value === undefined || value.trim() === '' ? defaultValue : Number(value)))\n .refine((value) => Number.isFinite(value) && value >= min && value <= max, {\n message: `must be a number between ${min} and ${max}`,\n })\n\nconst bigintValue = (defaultValue: bigint) =>\n z\n .string()\n .optional()\n .transform((value) => (value === undefined || value.trim() === '' ? defaultValue : BigInt(value.trim())))\n\nconst csv = z\n .string()\n .optional()\n .transform((value) =>\n (value ?? '')\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n )\n\nconst addressValue = z\n .string()\n .regex(/^0x[0-9a-fA-F]{40}$/, 'must be a 0x-prefixed address')\n .optional()\n\nconst envSchema = z.object({\n NETWORK: z.enum(['mainnet', 'testnet']).optional().default('mainnet'),\n ROBINHOOD_RPC_URL: z.string().url().optional(),\n DB_PATH: z.string().optional().default('./data/hood-alerts.sqlite'),\n PORT: int(8080, 1, 65535),\n LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),\n DRY_RUN: bool(false),\n\n TELEGRAM_BOT_TOKEN: z.string().optional(),\n DISCORD_BOT_TOKEN: z.string().optional(),\n DISCORD_APPLICATION_ID: z.string().optional(),\n DISCORD_PUBLIC_KEY: z.string().optional(),\n DISCORD_INTERACTIONS_PATH: z.string().optional().default('/discord/interactions'),\n\n POLL_INTERVAL_MS: int(3_000, 250, 600_000),\n FLUSH_INTERVAL_MS: int(1_000, 100, 600_000),\n CONFIRMATIONS: bigintValue(2n),\n CHUNK_SIZE: bigintValue(2_000n),\n INITIAL_LOOKBACK_BLOCKS: bigintValue(5_000n),\n\n WHALE_MIN_USD: int(1_000, 0, 1_000_000_000),\n WHALE_POOL_LIMIT: int(400, 1, 5_000),\n\n FLUSH_BATCH_SIZE: int(50, 1, 1_000),\n MAX_DELIVERY_ATTEMPTS: int(6, 1, 50),\n RETRY_BACKOFF_MS: int(30_000, 100, 3_600_000),\n OUTBOX_RETENTION_HOURS: int(72, 1, 8_760),\n\n ENTITLEMENTS: z.enum(['static', 'usdg', 'both']).optional().default('static'),\n PREMIUM_SUBSCRIBERS: csv,\n ALL_PREMIUM: bool(false),\n USDG_RECEIVER: addressValue,\n PREMIUM_PRICE_USDG: int(25, 1, 1_000_000),\n PREMIUM_PERIOD_DAYS: int(30, 1, 3_650),\n PAYMENTS_FROM_BLOCK: bigintValue(0n),\n UPGRADE_INSTRUCTIONS: z.string().optional(),\n})\n\n/** The validated service configuration. */\nexport interface ServiceConfig {\n network: 'mainnet' | 'testnet'\n rpcUrl: string | undefined\n dbPath: string\n port: number\n logLevel: LogLevel\n dryRun: boolean\n\n telegramBotToken: string | undefined\n discordBotToken: string | undefined\n discordApplicationId: string | undefined\n discordPublicKey: string | undefined\n discordInteractionsPath: string\n\n pollIntervalMs: number\n flushIntervalMs: number\n confirmations: bigint\n chunkSize: bigint\n initialLookbackBlocks: bigint\n\n whaleMinUsd: number\n whalePoolLimit: number\n\n flushBatchSize: number\n maxDeliveryAttempts: number\n retryBackoffMs: number\n outboxRetentionHours: number\n\n entitlements: 'static' | 'usdg' | 'both'\n premiumSubscribers: string[]\n allPremium: boolean\n usdgReceiver: `0x${string}` | undefined\n premiumPriceUsdg: number\n premiumPeriodDays: number\n paymentsFromBlock: bigint\n upgradeInstructions: string | undefined\n}\n\n/**\n * Load and validate configuration from the environment.\n *\n * @throws with every problem listed at once, so a misconfigured deployment\n * needs one fix round rather than one per variable.\n *\n * @example\n * ```ts\n * const config = loadConfig()\n * console.log(`watching ${config.network} with ${config.confirmations} confirmations`)\n * ```\n */\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): ServiceConfig {\n const parsed = envSchema.safeParse(env)\n if (!parsed.success) {\n const detail = parsed.error.issues\n .map((issue) => `${issue.path.join('.') || 'env'}: ${issue.message}`)\n .join('\\n ')\n throw new Error(`hood-alerts: invalid configuration\\n ${detail}`)\n }\n const value = parsed.data\n\n if (value.ENTITLEMENTS !== 'static' && !value.USDG_RECEIVER) {\n throw new Error(\n 'hood-alerts: ENTITLEMENTS=usdg (or both) needs USDG_RECEIVER, the address subscribers pay.',\n )\n }\n if (value.DISCORD_APPLICATION_ID && !value.DISCORD_PUBLIC_KEY) {\n throw new Error(\n 'hood-alerts: DISCORD_APPLICATION_ID is set but DISCORD_PUBLIC_KEY is not. Interactions cannot be verified without it.',\n )\n }\n\n return {\n network: value.NETWORK,\n rpcUrl: value.ROBINHOOD_RPC_URL,\n dbPath: value.DB_PATH,\n port: value.PORT,\n logLevel: value.LOG_LEVEL,\n dryRun: value.DRY_RUN,\n\n telegramBotToken: value.TELEGRAM_BOT_TOKEN,\n discordBotToken: value.DISCORD_BOT_TOKEN,\n discordApplicationId: value.DISCORD_APPLICATION_ID,\n discordPublicKey: value.DISCORD_PUBLIC_KEY,\n discordInteractionsPath: value.DISCORD_INTERACTIONS_PATH,\n\n pollIntervalMs: value.POLL_INTERVAL_MS,\n flushIntervalMs: value.FLUSH_INTERVAL_MS,\n confirmations: value.CONFIRMATIONS,\n chunkSize: value.CHUNK_SIZE,\n initialLookbackBlocks: value.INITIAL_LOOKBACK_BLOCKS,\n\n whaleMinUsd: value.WHALE_MIN_USD,\n whalePoolLimit: value.WHALE_POOL_LIMIT,\n\n flushBatchSize: value.FLUSH_BATCH_SIZE,\n maxDeliveryAttempts: value.MAX_DELIVERY_ATTEMPTS,\n retryBackoffMs: value.RETRY_BACKOFF_MS,\n outboxRetentionHours: value.OUTBOX_RETENTION_HOURS,\n\n entitlements: value.ENTITLEMENTS,\n premiumSubscribers: value.PREMIUM_SUBSCRIBERS,\n allPremium: value.ALL_PREMIUM,\n usdgReceiver: value.USDG_RECEIVER as `0x${string}` | undefined,\n premiumPriceUsdg: value.PREMIUM_PRICE_USDG,\n premiumPeriodDays: value.PREMIUM_PERIOD_DAYS,\n paymentsFromBlock: value.PAYMENTS_FROM_BLOCK,\n upgradeInstructions: value.UPGRADE_INSTRUCTIONS,\n }\n}\n","import { createRequire } from 'node:module'\nimport { mkdirSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { getAddress, type Address } from 'viem'\nimport type { DatabaseSync as DatabaseSyncType } from 'node:sqlite'\nimport type { PoolEntry, PoolRegistry } from '../events/registry.js'\nimport type { Launchpad } from '../events/types.js'\nimport type { RateLimitStore } from '../rules/ratelimit.js'\nimport { parseSubscription, type Platform, type Subscription } from '../rules/schema.js'\nimport type { WalletLinkStore } from '../tiers/entitlements.js'\nimport { decode, encode } from './codec.js'\n\n// `node:sqlite` is a core module but is not in vite's static builtin list, so a\n// plain `import` gets mis-resolved under vitest (vite tries, and fails, to\n// bundle it as a bare package). `require()` is opaque to vite's ESM import\n// analysis, which sidesteps the issue; Node natively supports requiring core\n// modules from ESM through `createRequire`.\n//\n// The resolution base needs the `??`: in the CJS build there is no\n// `import.meta`, and the bundler's shim leaves `url` undefined, which\n// `createRequire` rejects outright. Any absolute path is a valid base here\n// because a core module never resolves relative to it.\nconst requireFromHere = createRequire(\n (import.meta.url as string | undefined) ?? `${process.cwd()}/`,\n)\nconst { DatabaseSync } = requireFromHere('node:sqlite') as typeof import('node:sqlite')\n\n/**\n * All durable service state, in one SQLite file, with no native dependency.\n *\n * The three invariants this schema exists to guarantee, in order of how badly\n * they bite when they are missing:\n *\n * 1. **A restart never double-sends.** Every potential delivery has a\n * deterministic primary key (`eventId:subscriptionId:ruleId`), and\n * `enqueue` is an `INSERT` that reports a uniqueness violation as \"already\n * queued\". Re-processing a block range after a crash regenerates the exact\n * same keys, so the second pass inserts nothing.\n * 2. **A restart never silently skips a range.** The block cursor advances\n * only after every event in a chunk has been enqueued and committed. A\n * crash mid-chunk leaves the cursor pointing at the start of that chunk, so\n * the range is simply re-read.\n * 3. **A crash between enqueue and send does not lose the alert.** Enqueued\n * rows live in the outbox until they are delivered or dead-lettered. On\n * startup, rows left in `sending` (a crash mid-flight) are returned to\n * `pending` and retried.\n *\n * The one honest caveat: if the process dies after the platform accepted a\n * message but before the row was marked `sent`, that alert is delivered twice.\n * Neither the Telegram nor the Discord send API exposes a client-supplied\n * idempotency key, so this window cannot be closed from here. Everything\n * outside it is exactly once.\n */\n\n/** Delivery lifecycle. */\nexport type OutboxStatus = 'pending' | 'sending' | 'sent' | 'failed' | 'dead'\n\n/** A queued delivery. */\nexport interface OutboxRow {\n id: string\n subscriptionId: string\n subscriberId: string\n ruleId: string\n eventId: string\n eventJson: string\n platform: Platform\n target: string\n status: OutboxStatus\n /** Earliest delivery time, ms since epoch. Carries the free-tier delay. */\n notBeforeMs: number\n attempts: number\n lastError: string | null\n createdAtMs: number\n updatedAtMs: number\n}\n\n/** Aggregate counters for `/metrics`. */\nexport interface StoreMetrics {\n subscriptions: number\n enabledSubscriptions: number\n outbox: Record<OutboxStatus, number>\n pools: number\n deliveriesLastHour: number\n cursors: { sourceId: string; nextBlock: bigint }[]\n}\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS subscriptions (\n id TEXT PRIMARY KEY,\n subscriber_id TEXT NOT NULL,\n platform TEXT NOT NULL,\n target TEXT NOT NULL,\n enabled INTEGER NOT NULL DEFAULT 1,\n rules TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_subscriptions_subscriber ON subscriptions(subscriber_id);\n\nCREATE TABLE IF NOT EXISTS cursors (\n source_id TEXT PRIMARY KEY,\n next_block TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS outbox (\n id TEXT PRIMARY KEY,\n subscription_id TEXT NOT NULL,\n subscriber_id TEXT NOT NULL,\n rule_id TEXT NOT NULL,\n event_id TEXT NOT NULL,\n event_json TEXT NOT NULL,\n platform TEXT NOT NULL,\n target TEXT NOT NULL,\n status TEXT NOT NULL,\n not_before INTEGER NOT NULL,\n attempts INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_outbox_due ON outbox(status, not_before);\nCREATE INDEX IF NOT EXISTS idx_outbox_event ON outbox(event_id);\n\nCREATE TABLE IF NOT EXISTS deliveries (\n key TEXT NOT NULL,\n subscriber_id TEXT NOT NULL,\n at INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_deliveries_key ON deliveries(key, at);\nCREATE INDEX IF NOT EXISTS idx_deliveries_subscriber ON deliveries(subscriber_id, at);\n\nCREATE TABLE IF NOT EXISTS pools (\n pool TEXT PRIMARY KEY,\n token TEXT NOT NULL,\n quote_token TEXT NOT NULL,\n launchpad TEXT NOT NULL,\n created_block TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_pools_token ON pools(token);\nCREATE INDEX IF NOT EXISTS idx_pools_created ON pools(created_block);\n\nCREATE TABLE IF NOT EXISTS wallet_links (\n subscriber_id TEXT PRIMARY KEY,\n address TEXT NOT NULL,\n linked_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS link_nonces (\n subscriber_id TEXT PRIMARY KEY,\n nonce TEXT NOT NULL,\n issued_at INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);\n`\n\ninterface SubscriptionRow {\n id: string\n subscriber_id: string\n platform: string\n target: string\n enabled: number\n rules: string\n created_at: number\n}\n\ninterface RawOutboxRow {\n id: string\n subscription_id: string\n subscriber_id: string\n rule_id: string\n event_id: string\n event_json: string\n platform: string\n target: string\n status: string\n not_before: number\n attempts: number\n last_error: string | null\n created_at: number\n updated_at: number\n}\n\n/** The service's durable state. */\nexport class AlertStore {\n private readonly db: DatabaseSyncType\n private readonly clock: () => number\n\n constructor(path: string, clock: () => number = () => Date.now()) {\n if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true })\n this.db = new DatabaseSync(path)\n this.clock = clock\n this.db.exec('PRAGMA journal_mode = WAL;')\n this.db.exec('PRAGMA foreign_keys = ON;')\n this.db.exec(SCHEMA)\n }\n\n /** Deterministic outbox key. The whole dedupe guarantee rests on this. */\n static deliveryKey(eventId: string, subscriptionId: string, ruleId: string): string {\n return `${eventId}|${subscriptionId}|${ruleId}`\n }\n\n // ---- cursors -----------------------------------------------------------\n\n /** The next unprocessed block for a source, or `null` when never persisted. */\n getCursor(sourceId: string): bigint | null {\n const row = this.db.prepare('SELECT next_block FROM cursors WHERE source_id = ?').get(sourceId) as\n | { next_block: string }\n | undefined\n return row ? BigInt(row.next_block) : null\n }\n\n /** Persist the next unprocessed block. Call only after the range is enqueued. */\n setCursor(sourceId: string, nextBlock: bigint): void {\n this.db\n .prepare(\n `INSERT INTO cursors (source_id, next_block, updated_at) VALUES (?, ?, ?)\n ON CONFLICT(source_id) DO UPDATE SET next_block = excluded.next_block, updated_at = excluded.updated_at`,\n )\n .run(sourceId, nextBlock.toString(), this.clock())\n }\n\n /** Every persisted cursor, for `/metrics` and for operator inspection. */\n listCursors(): { sourceId: string; nextBlock: bigint }[] {\n const rows = this.db.prepare('SELECT source_id, next_block FROM cursors ORDER BY source_id').all() as Array<{\n source_id: string\n next_block: string\n }>\n return rows.map((row) => ({ sourceId: row.source_id, nextBlock: BigInt(row.next_block) }))\n }\n\n // ---- subscriptions -----------------------------------------------------\n\n /** Insert or replace a subscription. */\n saveSubscription(subscription: Subscription): void {\n const now = this.clock()\n this.db\n .prepare(\n `INSERT INTO subscriptions (id, subscriber_id, platform, target, enabled, rules, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n subscriber_id = excluded.subscriber_id,\n platform = excluded.platform,\n target = excluded.target,\n enabled = excluded.enabled,\n rules = excluded.rules,\n updated_at = excluded.updated_at`,\n )\n .run(\n subscription.id,\n subscription.subscriberId,\n subscription.platform,\n subscription.target,\n subscription.enabled ? 1 : 0,\n encode(subscription.rules),\n subscription.createdAtMs,\n now,\n )\n }\n\n private hydrate(row: SubscriptionRow): Subscription {\n return parseSubscription({\n id: row.id,\n subscriberId: row.subscriber_id,\n platform: row.platform,\n target: row.target,\n enabled: row.enabled === 1,\n rules: decode(row.rules),\n createdAtMs: row.created_at,\n })\n }\n\n /** One subscription by id. */\n getSubscription(id: string): Subscription | null {\n const row = this.db.prepare('SELECT * FROM subscriptions WHERE id = ?').get(id) as\n | SubscriptionRow\n | undefined\n return row ? this.hydrate(row) : null\n }\n\n /** Every subscription owned by a subscriber. */\n listSubscriptionsFor(subscriberId: string): Subscription[] {\n const rows = this.db\n .prepare('SELECT * FROM subscriptions WHERE subscriber_id = ? ORDER BY created_at')\n .all(subscriberId) as unknown as SubscriptionRow[]\n return rows.map((row) => this.hydrate(row))\n }\n\n /** Every enabled subscription, for the dispatch fan-out. */\n listEnabledSubscriptions(): Subscription[] {\n const rows = this.db\n .prepare('SELECT * FROM subscriptions WHERE enabled = 1 ORDER BY created_at')\n .all() as unknown as SubscriptionRow[]\n return rows.map((row) => this.hydrate(row))\n }\n\n /** Remove a subscription. Returns `true` when a row was deleted. */\n deleteSubscription(id: string): boolean {\n const result = this.db.prepare('DELETE FROM subscriptions WHERE id = ?').run(id)\n return Number(result.changes) > 0\n }\n\n /** Every token on any enabled subscription's watchlist, deduped. */\n watchlistTokens(): Address[] {\n const seen = new Set<string>()\n for (const subscription of this.listEnabledSubscriptions()) {\n for (const rule of subscription.rules) {\n for (const token of rule.tokens) seen.add(token.toLowerCase())\n }\n }\n return [...seen].map((token) => getAddress(token as Address))\n }\n\n // ---- outbox ------------------------------------------------------------\n\n /**\n * Queue a delivery.\n *\n * @returns `true` when this call created the row, `false` when it already\n * existed (a replayed block range, a duplicate log). The boolean is the\n * dedupe signal the dispatcher counts.\n */\n enqueue(row: Omit<OutboxRow, 'status' | 'attempts' | 'lastError' | 'createdAtMs' | 'updatedAtMs'>): boolean {\n const now = this.clock()\n try {\n this.db\n .prepare(\n `INSERT INTO outbox (id, subscription_id, subscriber_id, rule_id, event_id, event_json,\n platform, target, status, not_before, attempts, last_error, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, 0, NULL, ?, ?)`,\n )\n .run(\n row.id,\n row.subscriptionId,\n row.subscriberId,\n row.ruleId,\n row.eventId,\n row.eventJson,\n row.platform,\n row.target,\n row.notBeforeMs,\n now,\n now,\n )\n return true\n } catch (error) {\n // A UNIQUE violation is the expected path for a replay. Anything else is\n // a real storage failure and must not be swallowed.\n const message = error instanceof Error ? error.message : String(error)\n if (/UNIQUE|PRIMARY KEY/i.test(message)) return false\n throw error\n }\n }\n\n private static toOutboxRow(raw: RawOutboxRow): OutboxRow {\n return {\n id: raw.id,\n subscriptionId: raw.subscription_id,\n subscriberId: raw.subscriber_id,\n ruleId: raw.rule_id,\n eventId: raw.event_id,\n eventJson: raw.event_json,\n platform: raw.platform as Platform,\n target: raw.target,\n status: raw.status as OutboxStatus,\n notBeforeMs: raw.not_before,\n attempts: raw.attempts,\n lastError: raw.last_error,\n createdAtMs: raw.created_at,\n updatedAtMs: raw.updated_at,\n }\n }\n\n /**\n * Claim due deliveries, moving them to `sending` in one transaction so a\n * second flush pass cannot pick up the same rows.\n */\n claimDue(nowMs: number, limit: number): OutboxRow[] {\n const rows = this.db\n .prepare(\n `SELECT * FROM outbox\n WHERE status IN ('pending', 'failed') AND not_before <= ?\n ORDER BY not_before, created_at\n LIMIT ?`,\n )\n .all(nowMs, limit) as unknown as RawOutboxRow[]\n if (rows.length === 0) return []\n\n const update = this.db.prepare(`UPDATE outbox SET status = 'sending', updated_at = ? WHERE id = ?`)\n this.db.exec('BEGIN')\n try {\n for (const row of rows) update.run(nowMs, row.id)\n this.db.exec('COMMIT')\n } catch (error) {\n this.db.exec('ROLLBACK')\n throw error\n }\n return rows.map((row) => AlertStore.toOutboxRow(row))\n }\n\n /** Mark a delivery sent. */\n markSent(id: string): void {\n this.db\n .prepare(`UPDATE outbox SET status = 'sent', last_error = NULL, updated_at = ? WHERE id = ?`)\n .run(this.clock(), id)\n }\n\n /** Record a retryable failure and schedule the next attempt. */\n markFailed(id: string, attempts: number, error: string, nextAttemptAtMs: number): void {\n this.db\n .prepare(\n `UPDATE outbox SET status = 'failed', attempts = ?, last_error = ?, not_before = ?, updated_at = ? WHERE id = ?`,\n )\n .run(attempts, error, nextAttemptAtMs, this.clock(), id)\n }\n\n /** Give up on a delivery (permanent rejection, or attempts exhausted). */\n markDead(id: string, attempts: number, error: string): void {\n this.db\n .prepare(`UPDATE outbox SET status = 'dead', attempts = ?, last_error = ?, updated_at = ? WHERE id = ?`)\n .run(attempts, error, this.clock(), id)\n }\n\n /**\n * Return rows abandoned mid-flight by a crash to the queue.\n *\n * @returns how many rows were recovered.\n */\n recoverInFlight(): number {\n const result = this.db\n .prepare(`UPDATE outbox SET status = 'pending', updated_at = ? WHERE status = 'sending'`)\n .run(this.clock())\n return Number(result.changes)\n }\n\n /** Delete delivered rows older than `beforeMs`, keeping the file bounded. */\n pruneOutbox(beforeMs: number): number {\n const result = this.db\n .prepare(`DELETE FROM outbox WHERE status = 'sent' AND updated_at < ?`)\n .run(beforeMs)\n return Number(result.changes)\n }\n\n /** Delete delivery history older than `beforeMs`. */\n pruneDeliveries(beforeMs: number): number {\n const result = this.db.prepare('DELETE FROM deliveries WHERE at < ?').run(beforeMs)\n return Number(result.changes)\n }\n\n // ---- rate limiting and throughput --------------------------------------\n\n /** A {@link RateLimitStore} backed by the `deliveries` table. */\n rateLimitStore(): RateLimitStore {\n return {\n countSince: async (key: string, sinceMs: number): Promise<number> => {\n const row = this.db\n .prepare('SELECT COUNT(*) AS n FROM deliveries WHERE key = ? AND at >= ?')\n .get(key, sinceMs) as { n: number }\n return Number(row.n)\n },\n lastAt: async (key: string): Promise<number | null> => {\n const row = this.db\n .prepare('SELECT MAX(at) AS last FROM deliveries WHERE key = ?')\n .get(key) as { last: number | null }\n return row.last ?? null\n },\n record: async (key: string, subscriberId: string, atMs: number): Promise<void> => {\n this.recordDelivery(key, subscriberId, atMs)\n },\n countForSubscriberSince: async (subscriberId: string, sinceMs: number): Promise<number> => {\n return this.countDeliveriesForSubscriber(subscriberId, sinceMs)\n },\n }\n }\n\n /** Record a delivery for both rate limiting and throughput accounting. */\n recordDelivery(key: string, subscriberId: string, atMs: number): void {\n this.db.prepare('INSERT INTO deliveries (key, subscriber_id, at) VALUES (?, ?, ?)').run(key, subscriberId, atMs)\n }\n\n /** How many alerts a subscriber received since `sinceMs`. */\n countDeliveriesForSubscriber(subscriberId: string, sinceMs: number): number {\n const row = this.db\n .prepare('SELECT COUNT(*) AS n FROM deliveries WHERE subscriber_id = ? AND at >= ?')\n .get(subscriberId, sinceMs) as { n: number }\n return Number(row.n)\n }\n\n // ---- pool registry -----------------------------------------------------\n\n /** A {@link PoolRegistry} backed by the `pools` table. */\n poolRegistry(): PoolRegistry {\n return {\n record: async (entry: PoolEntry): Promise<void> => {\n this.db\n .prepare(\n `INSERT INTO pools (pool, token, quote_token, launchpad, created_block) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(pool) DO UPDATE SET\n token = excluded.token, quote_token = excluded.quote_token,\n launchpad = excluded.launchpad, created_block = excluded.created_block`,\n )\n .run(\n getAddress(entry.pool),\n getAddress(entry.token),\n getAddress(entry.quoteToken),\n entry.launchpad,\n entry.createdBlock.toString(),\n )\n },\n active: async (limit: number, pinned: readonly Address[] = []): Promise<PoolEntry[]> => {\n const picked = new Map<string, PoolEntry>()\n if (pinned.length > 0) {\n const placeholders = pinned.map(() => '?').join(',')\n const rows = this.db\n .prepare(`SELECT * FROM pools WHERE lower(token) IN (${placeholders})`)\n .all(...pinned.map((token) => token.toLowerCase())) as Array<Record<string, string>>\n for (const row of rows) {\n const entry = AlertStore.toPoolEntry(row)\n picked.set(entry.pool.toLowerCase(), entry)\n }\n }\n const rows = this.db\n .prepare(\n // created_block is stored as text to keep bigint precision, so it\n // is ordered numerically by length first, then lexically.\n 'SELECT * FROM pools ORDER BY length(created_block) DESC, created_block DESC LIMIT ?',\n )\n .all(limit) as Array<Record<string, string>>\n for (const row of rows) {\n if (picked.size >= limit + pinned.length) break\n const entry = AlertStore.toPoolEntry(row)\n if (!picked.has(entry.pool.toLowerCase())) picked.set(entry.pool.toLowerCase(), entry)\n }\n return [...picked.values()]\n },\n originOf: async (token: Address): Promise<Launchpad | null> => {\n const row = this.db\n .prepare('SELECT launchpad FROM pools WHERE lower(token) = ? LIMIT 1')\n .get(token.toLowerCase()) as { launchpad: string } | undefined\n return row ? (row.launchpad as Launchpad) : null\n },\n poolsFor: async (token: Address): Promise<PoolEntry[]> => {\n const rows = this.db\n .prepare('SELECT * FROM pools WHERE lower(token) = ?')\n .all(token.toLowerCase()) as Array<Record<string, string>>\n return rows.map((row) => AlertStore.toPoolEntry(row))\n },\n size: async (): Promise<number> => {\n const row = this.db.prepare('SELECT COUNT(*) AS n FROM pools').get() as { n: number }\n return Number(row.n)\n },\n }\n }\n\n private static toPoolEntry(row: Record<string, string>): PoolEntry {\n return {\n pool: getAddress(row['pool'] as Address),\n token: getAddress(row['token'] as Address),\n quoteToken: getAddress(row['quote_token'] as Address),\n launchpad: row['launchpad'] as Launchpad,\n createdBlock: BigInt(row['created_block'] as string),\n }\n }\n\n // ---- wallet links ------------------------------------------------------\n\n /** Store a signature-verified wallet link. */\n linkWallet(subscriberId: string, address: Address): void {\n this.db\n .prepare(\n `INSERT INTO wallet_links (subscriber_id, address, linked_at) VALUES (?, ?, ?)\n ON CONFLICT(subscriber_id) DO UPDATE SET address = excluded.address, linked_at = excluded.linked_at`,\n )\n .run(subscriberId, getAddress(address), this.clock())\n }\n\n /** A {@link WalletLinkStore} view of the links table. */\n walletLinks(): WalletLinkStore {\n return {\n walletOf: async (subscriberId: string): Promise<Address | null> => {\n const row = this.db\n .prepare('SELECT address FROM wallet_links WHERE subscriber_id = ?')\n .get(subscriberId) as { address: string } | undefined\n return row ? getAddress(row.address as Address) : null\n },\n }\n }\n\n /** Issue (or reissue) the nonce a subscriber must sign to link a wallet. */\n issueLinkNonce(subscriberId: string, nonce: string): string {\n this.db\n .prepare(\n `INSERT INTO link_nonces (subscriber_id, nonce, issued_at) VALUES (?, ?, ?)\n ON CONFLICT(subscriber_id) DO UPDATE SET nonce = excluded.nonce, issued_at = excluded.issued_at`,\n )\n .run(subscriberId, nonce, this.clock())\n return nonce\n }\n\n /** The outstanding link nonce for a subscriber, or `null`. */\n getLinkNonce(subscriberId: string): string | null {\n const row = this.db\n .prepare('SELECT nonce FROM link_nonces WHERE subscriber_id = ?')\n .get(subscriberId) as { nonce: string } | undefined\n return row?.nonce ?? null\n }\n\n /** Consume a link nonce so a signature cannot be replayed. */\n clearLinkNonce(subscriberId: string): void {\n this.db.prepare('DELETE FROM link_nonces WHERE subscriber_id = ?').run(subscriberId)\n }\n\n // ---- meta and metrics --------------------------------------------------\n\n /** Read a free-form service setting. */\n getMeta(key: string): string | null {\n const row = this.db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as\n | { value: string }\n | undefined\n return row?.value ?? null\n }\n\n /** Write a free-form service setting. */\n setMeta(key: string, value: string): void {\n this.db\n .prepare(\n `INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`,\n )\n .run(key, value)\n }\n\n /** Aggregate counters for `/metrics`. */\n metrics(): StoreMetrics {\n const outbox: Record<OutboxStatus, number> = { pending: 0, sending: 0, sent: 0, failed: 0, dead: 0 }\n const statusRows = this.db\n .prepare('SELECT status, COUNT(*) AS n FROM outbox GROUP BY status')\n .all() as Array<{ status: OutboxStatus; n: number }>\n for (const row of statusRows) outbox[row.status] = Number(row.n)\n\n const subs = this.db\n .prepare('SELECT COUNT(*) AS total, SUM(enabled) AS enabled FROM subscriptions')\n .get() as { total: number; enabled: number | null }\n const pools = this.db.prepare('SELECT COUNT(*) AS n FROM pools').get() as { n: number }\n const deliveries = this.db\n .prepare('SELECT COUNT(*) AS n FROM deliveries WHERE at >= ?')\n .get(this.clock() - 3_600_000) as { n: number }\n\n return {\n subscriptions: Number(subs.total),\n enabledSubscriptions: Number(subs.enabled ?? 0),\n outbox,\n pools: Number(pools.n),\n deliveriesLastHour: Number(deliveries.n),\n cursors: this.listCursors(),\n }\n }\n\n /** Close the database. Safe to call twice. */\n close(): void {\n try {\n this.db.close()\n } catch {\n // Already closed.\n }\n }\n}\n","import type { AlertEvent } from '../events/types.js'\n\n/**\n * JSON codec for alert events.\n *\n * Events carry `bigint` block numbers and raw token amounts, which\n * `JSON.stringify` refuses to serialize. Coercing them to `number` would\n * silently lose precision on any amount above 2^53 (routine for an 18-decimal\n * token), so bigints are tagged and restored exactly.\n */\n\nconst BIGINT_TAG = '$bigint'\n\n/** Serialize any value containing bigints. */\nexport function encode(value: unknown): string {\n return JSON.stringify(value, (_key, raw: unknown) =>\n typeof raw === 'bigint' ? { [BIGINT_TAG]: raw.toString() } : raw,\n )\n}\n\n/** Restore a value serialized by {@link encode}. */\nexport function decode<T>(text: string): T {\n return JSON.parse(text, (_key, raw: unknown) => {\n if (\n typeof raw === 'object' &&\n raw !== null &&\n Object.keys(raw).length === 1 &&\n typeof (raw as Record<string, unknown>)[BIGINT_TAG] === 'string'\n ) {\n return BigInt((raw as Record<string, string>)[BIGINT_TAG] as string)\n }\n return raw\n }) as T\n}\n\n/** Serialize an alert event for the outbox. */\nexport function encodeEvent(event: AlertEvent): string {\n return encode(event)\n}\n\n/** Restore an alert event from the outbox. */\nexport function decodeEvent(text: string): AlertEvent {\n return decode<AlertEvent>(text)\n}\n","import type { EventSource } from '../events/sources.js'\nimport type { AlertEvent } from '../events/types.js'\nimport { matchRules, type EvaluationContext } from '../rules/engine.js'\nimport { checkRateLimit, rateLimitKey, type RateLimitStore } from '../rules/ratelimit.js'\nimport type { Subscription } from '../rules/schema.js'\nimport { renderAlert, type RenderOptions } from '../notifiers/format.js'\nimport type { Notifier } from '../notifiers/types.js'\nimport type { EntitlementGate } from '../tiers/enforce.js'\nimport { decodeEvent, encodeEvent } from './codec.js'\nimport type { Logger } from './logger.js'\nimport { AlertStore, type OutboxRow } from './store.js'\n\n/**\n * The dispatcher: chain to inbox, in two independent halves.\n *\n * **`pollOnce`** reads confirmed block ranges, fans events out across\n * subscriptions and rules, and writes matched deliveries into the outbox. It\n * advances a source's cursor only after every event in the range has been\n * enqueued and committed, which is the property that makes a crash safe:\n * re-reading a range regenerates identical delivery ids and inserts nothing.\n *\n * **`flushOnce`** drains the outbox: claim due rows, render, send, mark the\n * outcome, retry retryable failures with backoff, dead-letter permanent ones.\n *\n * Splitting them is what lets the free tier have a real delivery delay (the\n * row carries `not_before`), lets a Telegram outage stall delivery without\n * stalling ingestion, and keeps a slow platform from pushing the block cursor\n * behind the chain.\n */\n\n/** What one `pollOnce` did. */\nexport interface PollStats {\n /** Blocks covered per source. */\n ranges: { sourceId: string; from: bigint; to: bigint }[]\n eventsDecoded: number\n /** Deliveries newly written to the outbox. */\n enqueued: number\n /** Matches that were already in the outbox (a replayed range). */\n deduped: number\n /** Matches suppressed by a rule rate limit or a tier throughput cap. */\n suppressed: number\n /** Sources that failed this pass. Their cursors did not advance. */\n errors: { sourceId: string; error: string }[]\n}\n\n/** What one `flushOnce` did. */\nexport interface FlushStats {\n claimed: number\n sent: number\n retried: number\n dead: number\n}\n\n/** Options for {@link createDispatcher}. */\nexport interface DispatcherOptions {\n store: AlertStore\n sources: readonly EventSource[]\n gate: EntitlementGate\n rateLimits: RateLimitStore\n /** Resolve the adapter for a subscription. */\n notifierFor: (subscription: Subscription) => Notifier | null\n /** Head block source. */\n getBlockNumber: () => Promise<bigint>\n logger: Logger\n /** Lazy lookups for liquidity and reputation filters. */\n evaluation?: EvaluationContext\n /** Telegram parse mode and footer options. */\n render?: RenderOptions\n /** Confirmations before a block is processed. @defaultValue `2n` */\n confirmations?: bigint\n /** Max blocks per poll per source. @defaultValue `2_000n` */\n chunkSize?: bigint\n /**\n * Where a source starts when it has no persisted cursor: this many blocks\n * behind the head. A fresh deployment should not replay months of history\n * into a subscriber's chat. @defaultValue `5_000n`\n */\n initialLookbackBlocks?: bigint\n /** Max outbox rows drained per flush. @defaultValue `50` */\n flushBatchSize?: number\n /** Attempts before a delivery is dead-lettered. @defaultValue `6` */\n maxDeliveryAttempts?: number\n /** Base retry backoff in ms, doubled per attempt. @defaultValue `30_000` */\n retryBackoffMs?: number\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\n/** The dispatcher. */\nexport interface Dispatcher {\n /** Ingest one confirmed range per source. */\n pollOnce(): Promise<PollStats>\n /** Deliver one batch of due outbox rows. */\n flushOnce(): Promise<FlushStats>\n /** Return rows abandoned by a crash to the queue. Call once at startup. */\n recover(): number\n}\n\n/** Build the dispatcher. */\nexport function createDispatcher(options: DispatcherOptions): Dispatcher {\n const {\n store,\n sources,\n gate,\n rateLimits,\n notifierFor,\n getBlockNumber,\n logger,\n } = options\n const confirmations = options.confirmations ?? 2n\n const chunkSize = options.chunkSize ?? 2_000n\n const initialLookback = options.initialLookbackBlocks ?? 5_000n\n const flushBatchSize = options.flushBatchSize ?? 50\n const maxAttempts = options.maxDeliveryAttempts ?? 6\n const retryBackoffMs = options.retryBackoffMs ?? 30_000\n const now = options.now ?? Date.now\n const evaluation = options.evaluation ?? {}\n\n /**\n * Fan one event out to every enabled subscription and write the matches.\n * Returns the counters this event contributed.\n */\n async function fanOut(\n event: AlertEvent,\n subscriptions: readonly Subscription[],\n ): Promise<{ enqueued: number; deduped: number; suppressed: number }> {\n let enqueued = 0\n let deduped = 0\n let suppressed = 0\n\n for (const subscription of subscriptions) {\n const matches = await matchRules(subscription.rules, event, evaluation)\n if (matches.length === 0) continue\n\n const { policy } = await gate.resolve(subscription.subscriberId)\n for (const { rule } of matches) {\n // Tier gating happens here rather than at rule-creation time only,\n // because an entitlement can lapse after a rule was accepted.\n if (!policy.allowedKinds.includes(event.kind)) {\n suppressed += 1\n continue\n }\n\n const key = rateLimitKey(subscription.id, rule.id)\n const limit = await checkRateLimit(rateLimits, key, rule.rateLimit, now())\n if (!limit.allowed) {\n suppressed += 1\n logger.debug('rate limited', { subscription: subscription.id, rule: rule.id, reason: limit.reason })\n continue\n }\n\n const usedThisHour = await rateLimits.countForSubscriberSince(\n subscription.subscriberId,\n now() - 3_600_000,\n )\n const throughput = await gate.checkThroughput(subscription.subscriberId, usedThisHour)\n if (!throughput.allowed) {\n suppressed += 1\n logger.debug('throughput capped', {\n subscriber: subscription.subscriberId,\n reason: throughput.reason,\n })\n continue\n }\n\n const id = AlertStore.deliveryKey(event.id, subscription.id, rule.id)\n const created = store.enqueue({\n id,\n subscriptionId: subscription.id,\n subscriberId: subscription.subscriberId,\n ruleId: rule.id,\n eventId: event.id,\n eventJson: encodeEvent(event),\n platform: subscription.platform,\n target: subscription.target,\n notBeforeMs: now() + policy.deliveryDelayMs,\n })\n if (!created) {\n deduped += 1\n continue\n }\n enqueued += 1\n // An enqueued alert is committed to being sent, so it consumes quota\n // now. Counting at send time instead would let one block's worth of\n // events blow straight through every cap before the first delivery.\n await rateLimits.record(key, subscription.subscriberId, now())\n }\n }\n return { enqueued, deduped, suppressed }\n }\n\n return {\n recover(): number {\n const recovered = store.recoverInFlight()\n if (recovered > 0) logger.warn('recovered in-flight deliveries after restart', { recovered })\n return recovered\n },\n\n async pollOnce(): Promise<PollStats> {\n const stats: PollStats = {\n ranges: [],\n eventsDecoded: 0,\n enqueued: 0,\n deduped: 0,\n suppressed: 0,\n errors: [],\n }\n const head = await getBlockNumber()\n const safeHead = head > confirmations ? head - confirmations : 0n\n const subscriptions = store.listEnabledSubscriptions()\n\n for (const source of sources) {\n const persisted = store.getCursor(source.id)\n const fallback = safeHead > initialLookback ? safeHead - initialLookback : source.startBlock\n const from = persisted ?? (fallback > source.startBlock ? fallback : source.startBlock)\n if (from > safeHead) continue\n const to = from + chunkSize - 1n > safeHead ? safeHead : from + chunkSize - 1n\n\n try {\n const events = await source.poll(from, to)\n stats.eventsDecoded += events.length\n for (const event of events) {\n const result = await fanOut(event, subscriptions)\n stats.enqueued += result.enqueued\n stats.deduped += result.deduped\n stats.suppressed += result.suppressed\n }\n // Committed only after every event in the range is in the outbox.\n store.setCursor(source.id, to + 1n)\n stats.ranges.push({ sourceId: source.id, from, to })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n stats.errors.push({ sourceId: source.id, error: message })\n logger.error('source poll failed, cursor not advanced', {\n source: source.id,\n from,\n to,\n error: message,\n })\n }\n }\n return stats\n },\n\n async flushOnce(): Promise<FlushStats> {\n const stats: FlushStats = { claimed: 0, sent: 0, retried: 0, dead: 0 }\n const rows = store.claimDue(now(), flushBatchSize)\n stats.claimed = rows.length\n\n for (const row of rows) {\n const outcome = await deliver(row)\n if (outcome === 'sent') stats.sent += 1\n else if (outcome === 'retry') stats.retried += 1\n else stats.dead += 1\n }\n return stats\n },\n }\n\n async function deliver(row: OutboxRow): Promise<'sent' | 'retry' | 'dead'> {\n const subscription = store.getSubscription(row.subscriptionId)\n if (!subscription) {\n store.markDead(row.id, row.attempts + 1, 'subscription no longer exists')\n return 'dead'\n }\n const notifier = notifierFor(subscription)\n if (!notifier) {\n store.markDead(row.id, row.attempts + 1, `no notifier configured for platform ${row.platform}`)\n return 'dead'\n }\n\n let event: AlertEvent\n try {\n event = decodeEvent(row.eventJson)\n } catch (error) {\n // An undecodable row can never succeed. Dead-letter it rather than\n // retrying a corrupt payload forever.\n store.markDead(row.id, row.attempts + 1, `corrupt event payload: ${String(error)}`)\n return 'dead'\n }\n\n const alert = renderAlert(event, {\n ...options.render,\n footer: options.render?.footer ?? `rule: ${row.ruleId}`,\n })\n const result = await notifier.send(row.target, alert)\n const attempts = row.attempts + 1\n\n if (result.ok) {\n store.markSent(row.id)\n logger.info('alert delivered', {\n id: row.id,\n platform: notifier.platform,\n kind: event.kind,\n token: event.token,\n attempts: result.attempts,\n })\n return 'sent'\n }\n\n if (!result.retryable || attempts >= maxAttempts) {\n store.markDead(row.id, attempts, result.error ?? 'delivery failed')\n logger.error('alert dead-lettered', {\n id: row.id,\n platform: notifier.platform,\n attempts,\n retryable: result.retryable,\n error: result.error,\n })\n return 'dead'\n }\n\n const nextAttemptAt = now() + retryBackoffMs * 2 ** (attempts - 1)\n store.markFailed(row.id, attempts, result.error ?? 'delivery failed', nextAttemptAt)\n logger.warn('alert delivery failed, will retry', {\n id: row.id,\n attempts,\n nextAttemptAt,\n error: result.error,\n })\n return 'retry'\n }\n}\n","import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'\nimport type { AlertStore } from './store.js'\nimport type { Logger } from './logger.js'\n\n/**\n * The service's HTTP surface: `/health`, `/ready`, `/metrics`, and the Discord\n * interactions endpoint when Discord is configured.\n *\n * Kept on `node:http` deliberately. The whole surface is four routes, and a\n * framework would add a dependency, a middleware stack and a body parser that\n * mangles the raw request body, which Discord's signature verification needs\n * byte for byte.\n */\n\n/** Liveness and readiness detail. */\nexport interface HealthState {\n /** Head block the poller last saw. `null` before the first successful poll. */\n headBlock: bigint | null\n /** When the poller last completed a pass, ms since epoch. */\n lastPollAtMs: number | null\n /** Last poll error, if the most recent pass failed. */\n lastError: string | null\n /** Which notifier platforms are configured. */\n platforms: string[]\n /** Version reported by `/health`. */\n version: string\n /** Process start time, ms since epoch. */\n startedAtMs: number\n}\n\n/** Options for {@link createHttpServer}. */\nexport interface HttpServerOptions {\n store: AlertStore\n logger: Logger\n /** Live health snapshot, read per request. */\n health: () => HealthState\n /**\n * Discord interactions handler, mounted at `interactionsPath`. Omitted when\n * Discord is not configured, in which case the path 404s.\n */\n discord?: {\n path: string\n handle(request: {\n signature: string | null\n timestamp: string | null\n rawBody: string\n }): Promise<{ status: number; body: unknown }>\n }\n /**\n * How stale the last successful poll may be before `/ready` reports 503, in\n * ms. @defaultValue `120_000`\n */\n readinessStalenessMs?: number\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n const payload = JSON.stringify(body, (_key, value: unknown) =>\n typeof value === 'bigint' ? value.toString() : value,\n )\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })\n res.end(payload)\n}\n\nasync function readBody(req: IncomingMessage, limitBytes: number): Promise<string | null> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))\n size += buffer.length\n if (size > limitBytes) return null\n chunks.push(buffer)\n }\n return Buffer.concat(chunks).toString('utf8')\n}\n\n/** Render Prometheus text-format metrics. */\nexport function renderMetrics(store: AlertStore, health: HealthState, nowMs: number): string {\n const metrics = store.metrics()\n const lines: string[] = [\n '# HELP hood_alerts_subscriptions Subscriptions by state.',\n '# TYPE hood_alerts_subscriptions gauge',\n `hood_alerts_subscriptions{state=\"total\"} ${metrics.subscriptions}`,\n `hood_alerts_subscriptions{state=\"enabled\"} ${metrics.enabledSubscriptions}`,\n '# HELP hood_alerts_outbox Deliveries by outbox status.',\n '# TYPE hood_alerts_outbox gauge',\n ]\n for (const [status, count] of Object.entries(metrics.outbox)) {\n lines.push(`hood_alerts_outbox{status=\"${status}\"} ${count}`)\n }\n lines.push(\n '# HELP hood_alerts_deliveries_last_hour Alerts delivered in the last rolling hour.',\n '# TYPE hood_alerts_deliveries_last_hour gauge',\n `hood_alerts_deliveries_last_hour ${metrics.deliveriesLastHour}`,\n '# HELP hood_alerts_pools Memecoin pools tracked for whale trades.',\n '# TYPE hood_alerts_pools gauge',\n `hood_alerts_pools ${metrics.pools}`,\n '# HELP hood_alerts_cursor_block Next unprocessed block per source.',\n '# TYPE hood_alerts_cursor_block gauge',\n )\n for (const cursor of metrics.cursors) {\n lines.push(`hood_alerts_cursor_block{source=\"${cursor.sourceId}\"} ${cursor.nextBlock}`)\n }\n lines.push(\n '# HELP hood_alerts_head_block Chain head block last observed.',\n '# TYPE hood_alerts_head_block gauge',\n `hood_alerts_head_block ${health.headBlock ?? 0}`,\n '# HELP hood_alerts_lag_blocks How far each source trails the chain head.',\n '# TYPE hood_alerts_lag_blocks gauge',\n )\n if (health.headBlock !== null) {\n for (const cursor of metrics.cursors) {\n const lag = health.headBlock > cursor.nextBlock ? health.headBlock - cursor.nextBlock : 0n\n lines.push(`hood_alerts_lag_blocks{source=\"${cursor.sourceId}\"} ${lag}`)\n }\n }\n lines.push(\n '# HELP hood_alerts_last_poll_age_seconds Seconds since the last successful poll.',\n '# TYPE hood_alerts_last_poll_age_seconds gauge',\n `hood_alerts_last_poll_age_seconds ${\n health.lastPollAtMs === null ? -1 : Math.round((nowMs - health.lastPollAtMs) / 1000)\n }`,\n '# HELP hood_alerts_uptime_seconds Process uptime.',\n '# TYPE hood_alerts_uptime_seconds counter',\n `hood_alerts_uptime_seconds ${Math.round((nowMs - health.startedAtMs) / 1000)}`,\n '',\n )\n return lines.join('\\n')\n}\n\n/** Build the HTTP server. Call `listen` yourself so startup order stays explicit. */\nexport function createHttpServer(options: HttpServerOptions): Server {\n const now = options.now ?? Date.now\n const stalenessMs = options.readinessStalenessMs ?? 120_000\n\n return createServer((req: IncomingMessage, res: ServerResponse) => {\n void (async () => {\n const url = new URL(req.url ?? '/', 'http://localhost')\n const path = url.pathname\n\n try {\n if (req.method === 'GET' && (path === '/health' || path === '/healthz')) {\n const health = options.health()\n json(res, 200, {\n ok: true,\n version: health.version,\n uptimeSeconds: Math.round((now() - health.startedAtMs) / 1000),\n headBlock: health.headBlock,\n lastPollAtMs: health.lastPollAtMs,\n lastError: health.lastError,\n platforms: health.platforms,\n })\n return\n }\n\n if (req.method === 'GET' && path === '/ready') {\n const health = options.health()\n // Ready means \"ingesting\": a service whose poller died is alive but\n // useless, and reporting it ready would hide the outage from an\n // orchestrator that could restart it.\n const fresh =\n health.lastPollAtMs !== null && now() - health.lastPollAtMs <= stalenessMs\n json(res, fresh ? 200 : 503, {\n ready: fresh,\n lastPollAtMs: health.lastPollAtMs,\n lastError: health.lastError,\n })\n return\n }\n\n if (req.method === 'GET' && path === '/metrics') {\n const body = renderMetrics(options.store, options.health(), now())\n res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8' })\n res.end(body)\n return\n }\n\n if (options.discord && req.method === 'POST' && path === options.discord.path) {\n const rawBody = await readBody(req, 256 * 1024)\n if (rawBody === null) {\n json(res, 413, { error: 'request body too large' })\n return\n }\n const answer = await options.discord.handle({\n signature: req.headers['x-signature-ed25519'] as string | undefined ?? null,\n timestamp: req.headers['x-signature-timestamp'] as string | undefined ?? null,\n rawBody,\n })\n json(res, answer.status, answer.body)\n return\n }\n\n json(res, 404, { error: `no route for ${req.method ?? 'GET'} ${path}` })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n options.logger.error('http handler failed', { path, error: message })\n if (!res.headersSent) json(res, 500, { error: 'internal error' })\n else res.end()\n }\n })()\n })\n}\n","import type { Server } from 'node:http'\nimport { createHoodClient, type HoodClient } from 'hoodchain'\nimport { createEventSources } from '../events/sources.js'\nimport { createLiquidityReader } from '../events/liquidity.js'\nimport { createPriceOracle } from '../events/pricing.js'\nimport { createTokenMetaReader } from '../events/tokens.js'\nimport { createCommandRouter } from '../bot/commands.js'\nimport { createDiscordInteractionHandler } from '../bot/discord-bot.js'\nimport { createTelegramBot, type TelegramBot } from '../bot/telegram-bot.js'\nimport { createCaptureNotifier } from '../notifiers/capture.js'\nimport { createDiscordBotNotifier, createDiscordWebhookNotifier } from '../notifiers/discord.js'\nimport { createTelegramClient, createTelegramNotifier } from '../notifiers/telegram.js'\nimport type { Notifier } from '../notifiers/types.js'\nimport { createRpcReputationProvider } from '../rules/reputation.js'\nimport type { Subscription } from '../rules/schema.js'\nimport { createEntitlementGate } from '../tiers/enforce.js'\nimport {\n chainEntitlementProviders,\n createStaticEntitlementProvider,\n createUsdgEntitlementProvider,\n type EntitlementProvider,\n} from '../tiers/entitlements.js'\nimport type { ServiceConfig } from './config.js'\nimport { createDispatcher, type Dispatcher } from './dispatcher.js'\nimport { createHttpServer, type HealthState } from './http.js'\nimport { createLogger, type Logger } from './logger.js'\nimport { AlertStore } from './store.js'\n\n/** Package version reported by `/health`. Kept in step with package.json. */\nexport const VERSION = '0.1.0'\n\n/** A wired, runnable service. */\nexport interface AlertService {\n readonly config: ServiceConfig\n readonly store: AlertStore\n readonly dispatcher: Dispatcher\n readonly logger: Logger\n /** Current health snapshot. */\n health(): HealthState\n /** Start the HTTP server, the poll loop, the flush loop and the bots. */\n start(): Promise<void>\n /** Stop everything and close the database. Safe to call twice. */\n stop(): Promise<void>\n}\n\n/** Options for {@link createService}, all optional except the config. */\nexport interface CreateServiceOptions {\n config: ServiceConfig\n /** Substitute the chain client (a test double, or a pre-configured client). */\n client?: HoodClient\n /** Substitute the logger. */\n logger?: Logger\n /** Substitute the store (`:memory:` in tests). */\n store?: AlertStore\n}\n\nfunction buildEntitlements(\n config: ServiceConfig,\n client: HoodClient,\n store: AlertStore,\n): EntitlementProvider {\n const staticProvider = createStaticEntitlementProvider({\n premiumSubscribers: config.premiumSubscribers,\n allPremium: config.allPremium,\n })\n if (config.entitlements === 'static') return staticProvider\n\n const usdgProvider = createUsdgEntitlementProvider({\n client,\n receiver: config.usdgReceiver as `0x${string}`,\n pricePerPeriodUsdg: config.premiumPriceUsdg,\n periodDays: config.premiumPeriodDays,\n links: store.walletLinks(),\n fromBlock: config.paymentsFromBlock,\n })\n if (config.entitlements === 'usdg') return usdgProvider\n // 'both': configuration grants comps, the chain sells subscriptions.\n return chainEntitlementProviders(staticProvider, usdgProvider)\n}\n\n/**\n * Wire the whole service from a validated configuration.\n *\n * @example\n * ```ts\n * import { createService, loadConfig } from 'hood-alerts/service'\n *\n * const service = createService({ config: loadConfig() })\n * await service.start()\n * ```\n */\nexport function createService(options: CreateServiceOptions): AlertService {\n const config = options.config\n const logger =\n options.logger ?? createLogger({ level: config.logLevel, base: { service: 'hood-alerts' } })\n const client =\n options.client ??\n createHoodClient({\n chain: config.network,\n ...(config.rpcUrl ? { rpcUrl: config.rpcUrl } : {}),\n })\n const store = options.store ?? new AlertStore(config.dbPath)\n\n const oracle = createPriceOracle(client)\n const tokens = createTokenMetaReader(client)\n const registry = store.poolRegistry()\n const liquidity = createLiquidityReader(client, oracle, registry)\n const reputation = createRpcReputationProvider(client, oracle)\n\n const sources = createEventSources({\n client,\n oracle,\n tokens,\n registry,\n pinnedTokens: async () => store.watchlistTokens(),\n whaleMinUsd: config.whaleMinUsd,\n whalePoolLimit: config.whalePoolLimit,\n })\n\n const entitlements = buildEntitlements(config, client, store)\n const gate = createEntitlementGate({ provider: entitlements })\n\n // ---- notifiers ---------------------------------------------------------\n const platforms: string[] = []\n let telegramNotifier: Notifier | null = null\n let discordWebhookNotifier: Notifier | null = null\n let discordBotNotifier: Notifier | null = null\n const telegramClient = config.telegramBotToken\n ? createTelegramClient({ botToken: config.telegramBotToken })\n : null\n\n if (config.dryRun) {\n telegramNotifier = createCaptureNotifier('telegram', (delivery) =>\n logger.info('dry run: telegram alert', { target: delivery.target, text: delivery.alert.plain }),\n )\n discordWebhookNotifier = createCaptureNotifier('discord', (delivery) =>\n logger.info('dry run: discord alert', { target: delivery.target, text: delivery.alert.plain }),\n )\n discordBotNotifier = discordWebhookNotifier\n platforms.push('dry-run')\n } else {\n if (telegramClient) {\n telegramNotifier = createTelegramNotifier({\n botToken: config.telegramBotToken as string,\n client: telegramClient,\n })\n platforms.push('telegram')\n }\n // The webhook adapter needs no credential, so Discord webhook delivery is\n // always available even without a bot token.\n discordWebhookNotifier = createDiscordWebhookNotifier({})\n platforms.push('discord-webhook')\n if (config.discordBotToken) {\n discordBotNotifier = createDiscordBotNotifier({ botToken: config.discordBotToken })\n platforms.push('discord-bot')\n }\n }\n\n function notifierFor(subscription: Subscription): Notifier | null {\n if (subscription.platform === 'telegram') return telegramNotifier\n if (subscription.target.startsWith('https://')) return discordWebhookNotifier\n return discordBotNotifier\n }\n\n const dispatcher = createDispatcher({\n store,\n sources,\n gate,\n rateLimits: store.rateLimitStore(),\n notifierFor,\n getBlockNumber: () => client.public.getBlockNumber(),\n logger,\n evaluation: {\n liquidityUsd: (event) => liquidity.tokenLiquidityUsd(event.token),\n reputation: (deployer) => reputation.get(deployer),\n isLpLocked: (positionId) => reputation.isLpLocked(positionId),\n },\n confirmations: config.confirmations,\n chunkSize: config.chunkSize,\n initialLookbackBlocks: config.initialLookbackBlocks,\n flushBatchSize: config.flushBatchSize,\n maxDeliveryAttempts: config.maxDeliveryAttempts,\n retryBackoffMs: config.retryBackoffMs,\n })\n\n // ---- bot ---------------------------------------------------------------\n const router = createCommandRouter({\n store,\n gate,\n ...(config.upgradeInstructions ? { upgradeInstructions: config.upgradeInstructions } : {}),\n })\n\n let telegramBot: TelegramBot | null = null\n if (telegramClient && !config.dryRun) {\n telegramBot = createTelegramBot({\n botToken: config.telegramBotToken as string,\n client: telegramClient,\n router,\n logger: logger.child({ component: 'telegram-bot' }),\n offsetStore: {\n get: () => {\n const raw = store.getMeta('telegram:offset')\n return raw === null ? null : Number(raw)\n },\n set: (offset: number) => store.setMeta('telegram:offset', String(offset)),\n },\n })\n }\n\n const discordHandler = config.discordPublicKey\n ? createDiscordInteractionHandler({ router, publicKey: config.discordPublicKey })\n : null\n\n // ---- health ------------------------------------------------------------\n const startedAtMs = Date.now()\n let headBlock: bigint | null = null\n let lastPollAtMs: number | null = null\n let lastError: string | null = null\n\n const health = (): HealthState => ({\n headBlock,\n lastPollAtMs,\n lastError,\n platforms,\n version: VERSION,\n startedAtMs,\n })\n\n const server = createHttpServer({\n store,\n logger: logger.child({ component: 'http' }),\n health,\n ...(discordHandler\n ? { discord: { path: config.discordInteractionsPath, handle: discordHandler.handle } }\n : {}),\n })\n\n // ---- loops -------------------------------------------------------------\n let running = false\n const loops: Promise<void>[] = []\n let stopping: Promise<void> | null = null\n const waiters = new Set<() => void>()\n\n /** Sleep that resolves early when the service is stopping. */\n function delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n waiters.delete(wake)\n resolve()\n }, ms)\n const wake = (): void => {\n clearTimeout(timer)\n waiters.delete(wake)\n resolve()\n }\n waiters.add(wake)\n })\n }\n\n async function pollLoop(): Promise<void> {\n while (running) {\n try {\n headBlock = await client.public.getBlockNumber()\n const stats = await dispatcher.pollOnce()\n lastPollAtMs = Date.now()\n lastError = stats.errors.length > 0 ? (stats.errors[0]?.error ?? null) : null\n if (stats.eventsDecoded > 0 || stats.enqueued > 0) {\n logger.info('poll complete', {\n head: headBlock,\n events: stats.eventsDecoded,\n enqueued: stats.enqueued,\n deduped: stats.deduped,\n suppressed: stats.suppressed,\n errors: stats.errors.length,\n })\n }\n } catch (error) {\n lastError = error instanceof Error ? error.message : String(error)\n logger.error('poll loop error', { error: lastError })\n }\n await delay(config.pollIntervalMs)\n }\n }\n\n async function flushLoop(): Promise<void> {\n while (running) {\n try {\n const stats = await dispatcher.flushOnce()\n if (stats.claimed > 0) logger.debug('flush complete', { ...stats })\n } catch (error) {\n logger.error('flush loop error', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n await delay(config.flushIntervalMs)\n }\n }\n\n async function pruneLoop(): Promise<void> {\n while (running) {\n await delay(3_600_000)\n if (!running) break\n try {\n const cutoff = Date.now() - config.outboxRetentionHours * 3_600_000\n const outbox = store.pruneOutbox(cutoff)\n // Delivery history only needs to outlive the longest rate-limit window.\n const deliveries = store.pruneDeliveries(Date.now() - 25 * 3_600_000)\n if (outbox > 0 || deliveries > 0) logger.info('pruned', { outbox, deliveries })\n } catch (error) {\n logger.error('prune failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n }\n\n return {\n config,\n store,\n dispatcher,\n logger,\n health,\n\n async start(): Promise<void> {\n if (running) return\n running = true\n dispatcher.recover()\n\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error): void => reject(error)\n server.once('error', onError)\n server.listen(config.port, () => {\n server.removeListener('error', onError)\n resolve()\n })\n })\n\n logger.info('service started', {\n port: config.port,\n network: config.network,\n dryRun: config.dryRun,\n platforms,\n entitlements: config.entitlements,\n sources: sources.map((source) => source.id),\n })\n\n loops.push(pollLoop(), flushLoop(), pruneLoop())\n if (telegramBot) {\n await telegramBot.publishCommands()\n loops.push(telegramBot.start())\n }\n },\n\n async stop(): Promise<void> {\n stopping ??= (async () => {\n logger.info('service stopping')\n running = false\n telegramBot?.stop()\n for (const wake of [...waiters]) wake()\n await new Promise<void>((resolve) => {\n server.close(() => resolve())\n // Keep-alive sockets would otherwise hold the close open for a full\n // idle timeout on a shutdown that should take milliseconds.\n server.closeAllConnections?.()\n })\n await Promise.allSettled(loops)\n store.close()\n logger.info('service stopped')\n })()\n return stopping\n },\n }\n}\n\n/** A server handle, exported for tests that need the raw `node:http` server. */\nexport type { Server }\n"],"mappings":"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","names":["import_viem","import_hoodchain","import_viem","import_viem","import_hoodchain","sleep","import_viem","import_hoodchain","import_viem","import_hoodchain","ODYSSEY_FACTORIES","import_viem","sleep","sleep","PERMANENT_STATUSES","import_viem","import_viem","import_hoodchain","json","delivered","nonce","reply","import_node_crypto","cryptoVerify","reply","import_zod","import_viem","eventId","rows","deliver","import_hoodchain"]}