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/rules/index.ts","../../src/rules/schema.ts","../../src/events/types.ts","../../src/rules/reputation.ts","../../src/events/logs.ts","../../src/events/pricing.ts","../../src/rules/engine.ts","../../src/rules/ratelimit.ts"],"sourcesContent":["/**\n * The rule engine: serializable, schema-validated per-subscription filters,\n * evaluated lazily against normalized alert events.\n *\n * @packageDocumentation\n */\n\nexport {\n addressSchema,\n canEmit,\n defaultRules,\n eventKindSchema,\n formatValidationError,\n launchpadSchema,\n parseRule,\n parseSubscription,\n platformSchema,\n rateLimitSchema,\n reputationFilterSchema,\n ruleComplexity,\n ruleSchema,\n safeParseRule,\n safeParseSubscription,\n sideSchema,\n subscriptionSchema,\n usesOnChainLookups,\n} from './schema.js'\nexport type {\n Platform,\n RateLimitConfig,\n ReputationFilter,\n Rule,\n RuleInput,\n Subscription,\n SubscriptionInput,\n} from './schema.js'\n\nexport { evaluateRule, matchRules } from './engine.js'\nexport type { EvaluationContext, MatchResult } from './engine.js'\n\nexport { checkRateLimit, createMemoryRateLimitStore, rateLimitKey } from './ratelimit.js'\nexport type { RateLimitDecision, RateLimitStore } from './ratelimit.js'\n\nexport { createRpcReputationProvider, priorLaunchesBefore } from './reputation.js'\nexport type {\n DeployerReputation,\n PriorLaunch,\n ReputationProvider,\n ReputationProviderOptions,\n} from './reputation.js'\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 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 { 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'\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 { 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 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"],"mappings":"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","names":["import_viem","import_hoodchain"]}
@@ -0,0 +1,166 @@
1
+ import { b as Rule } from '../schema-NnIdKFl2.cjs';
2
+ export { P as Platform, R as RateLimitConfig, a as ReputationFilter, c as RuleInput, S as Subscription, d as SubscriptionInput, e as addressSchema, f as canEmit, g as defaultRules, h as eventKindSchema, i as formatValidationError, l as launchpadSchema, p as parseRule, j as parseSubscription, k as platformSchema, r as rateLimitSchema, m as reputationFilterSchema, n as ruleComplexity, o as ruleSchema, s as safeParseRule, q as safeParseSubscription, t as sideSchema, u as subscriptionSchema, v as usesOnChainLookups } from '../schema-NnIdKFl2.cjs';
3
+ import { Address } from 'viem';
4
+ import { e as Launchpad, A as AlertEvent } from '../types-DFPpTMWo.cjs';
5
+ import { HoodClient } from 'hoodchain';
6
+ import { F as FetchLogRangeOptions } from '../logs-D7sYBvSy.cjs';
7
+ import { P as PriceOracle } from '../pricing-BumgLpvB.cjs';
8
+ export { R as RateLimitDecision, a as RateLimitStore, c as checkRateLimit, b as createMemoryRateLimitStore, r as rateLimitKey } from '../ratelimit-BKVQG2oY.cjs';
9
+ import 'zod';
10
+
11
+ /**
12
+ * Deployer reputation, derived entirely from chain state.
13
+ *
14
+ * Nothing here is scraped, self-reported or scored by a model. Three
15
+ * measurements, each with an exact on-chain definition:
16
+ *
17
+ * - **Prior launches**: `TokenLaunched` logs on the NOXA factory with
18
+ * `deployer` indexed to this address, plus `TokenCreated` logs on the three
19
+ * Odyssey factories with `creator` indexed to this address. Both queries are
20
+ * topic-selective, so the RPC serves them across the full chain history in
21
+ * one call each.
22
+ * - **LP locked**: for a NOXA launch, the LP position NFT is locked when
23
+ * `NonfungiblePositionManager.ownerOf(positionId)` is the NOXA locker
24
+ * contract. That is the launchpad's own permanent-lock mechanism, read
25
+ * directly rather than trusted from a UI badge.
26
+ * - **Rugged**: a prior launch whose pool quote-token reserve has since fallen
27
+ * below `rugThresholdUsd`. Liquidity that was seeded and is now gone is the
28
+ * observable footprint of a pull. It is a heuristic and is labelled as one
29
+ * everywhere it surfaces: a token that simply never traded and a token whose
30
+ * liquidity was withdrawn both end up with an empty pool, and only the
31
+ * second is a rug. Pair it with `minPriorLaunches` for a meaningful signal.
32
+ *
33
+ * Results are cached per deployer, because a launch burst from one deployer
34
+ * would otherwise re-run the same history scan per event.
35
+ */
36
+ /** One prior launch by a deployer. */
37
+ interface PriorLaunch {
38
+ launchpad: Launchpad;
39
+ token: Address;
40
+ pool: Address | null;
41
+ quoteToken: Address | null;
42
+ /** NOXA LP position id, when the launch had one. */
43
+ positionId: bigint | null;
44
+ blockNumber: bigint;
45
+ }
46
+ /** A deployer's on-chain track record. */
47
+ interface DeployerReputation {
48
+ deployer: Address;
49
+ /** Every launch found, oldest first. Capped by `maxHistory`. */
50
+ launches: PriorLaunch[];
51
+ /** Launches whose LP position is held by the NOXA locker. */
52
+ lockedLaunches: number;
53
+ /** Launches whose pool reserve is now below the rug threshold. */
54
+ ruggedLaunches: number;
55
+ /**
56
+ * How many launches were actually inspected for lock/rug status. Lower than
57
+ * `launches.length` when the history exceeds `maxInspect`, and surfaced so
58
+ * consumers never read a partial scan as a clean record.
59
+ */
60
+ inspected: number;
61
+ /** When this record was computed, ms since epoch. */
62
+ computedAtMs: number;
63
+ }
64
+ /** Reputation lookups for the rule engine. */
65
+ interface ReputationProvider {
66
+ get(deployer: Address): Promise<DeployerReputation>;
67
+ /** Is a specific LP position locked by the launchpad locker? */
68
+ isLpLocked(positionId: bigint): Promise<boolean>;
69
+ }
70
+ /** Options for {@link createRpcReputationProvider}. */
71
+ interface ReputationProviderOptions {
72
+ /** Cache lifetime per deployer, in ms. @defaultValue `600_000` (10 minutes) */
73
+ ttlMs?: number;
74
+ /** Max launches kept per deployer. @defaultValue `200` */
75
+ maxHistory?: number;
76
+ /**
77
+ * Max prior launches inspected for lock/rug status per deployer. Each one
78
+ * costs a pool balance read, so this bounds the RPC cost of a rule.
79
+ * @defaultValue `25`
80
+ */
81
+ maxInspect?: number;
82
+ /** Pool reserve below this USD value counts as drained. @defaultValue `50` */
83
+ rugThresholdUsd?: number;
84
+ /** Retry/bisect options for the history queries. */
85
+ logOptions?: FetchLogRangeOptions;
86
+ /** Clock injection point for tests. @defaultValue `Date.now` */
87
+ now?: () => number;
88
+ }
89
+ /**
90
+ * Build an RPC-backed reputation provider.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const reputation = createRpcReputationProvider(hood, oracle)
95
+ * const record = await reputation.get('0xDeployer…')
96
+ * console.log(record.launches.length, 'prior launches,', record.ruggedLaunches, 'drained')
97
+ * ```
98
+ */
99
+ declare function createRpcReputationProvider(client: HoodClient, oracle: PriceOracle, options?: ReputationProviderOptions): ReputationProvider;
100
+ /**
101
+ * Count a deployer's launches that happened strictly before a block. Rules ask
102
+ * "how many had they launched *at the time of this event*", which is not the
103
+ * same as "how many have they launched now", and only the first is stable when
104
+ * a historical range is re-processed after a restart.
105
+ */
106
+ declare function priorLaunchesBefore(record: DeployerReputation, blockNumber: bigint): number;
107
+
108
+ /**
109
+ * The rule engine: does this event satisfy this rule?
110
+ *
111
+ * Two properties matter more than the filter list itself.
112
+ *
113
+ * **Lazy, ordered evaluation.** Cheap in-memory filters (kind, launchpad,
114
+ * watchlists, USD bounds) run before anything that costs an RPC call
115
+ * (liquidity, reputation). A rule that rejects an event on its kind never
116
+ * triggers a pool balance read. With thousands of subscriptions this is the
117
+ * difference between a service that keeps up with the chain and one that does
118
+ * not.
119
+ *
120
+ * **Unknown is not a pass.** Every USD and liquidity figure can legitimately
121
+ * be `null` (no USDG or WETH leg to price against, an unreadable pool). A
122
+ * filter that asks for a measurement the chain did not give us does not match.
123
+ * The alternative, treating unknown as zero or as infinity, silently satisfies
124
+ * either `minUsd` or `maxLiquidityUsd` and ships alerts that are simply wrong.
125
+ */
126
+ /** Lazily-invoked lookups the engine uses only when a rule needs them. */
127
+ interface EvaluationContext {
128
+ /** USD liquidity of the event token's deepest known pool. */
129
+ liquidityUsd?: (event: AlertEvent) => Promise<number | null>;
130
+ /** On-chain track record for a deployer. */
131
+ reputation?: (deployer: Address) => Promise<DeployerReputation>;
132
+ /** Is this LP position held by the launchpad locker? */
133
+ isLpLocked?: (positionId: bigint) => Promise<boolean>;
134
+ }
135
+ /** The outcome of evaluating one rule against one event. */
136
+ interface MatchResult {
137
+ matched: boolean;
138
+ /**
139
+ * Why. On a match this names the rule; on a miss it names the first filter
140
+ * that rejected the event. The bot's `/status` and the service's debug log
141
+ * both surface this, so "why didn't I get an alert" is answerable.
142
+ */
143
+ reason: string;
144
+ }
145
+ /**
146
+ * Evaluate a rule against an event.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * const rule = parseRule({ id: 'whales', kinds: ['whale_trade'], minUsd: 5000 })
151
+ * const result = await evaluateRule(rule, event)
152
+ * if (!result.matched) console.log('skipped:', result.reason)
153
+ * ```
154
+ */
155
+ declare function evaluateRule(rule: Rule, event: AlertEvent, context?: EvaluationContext): Promise<MatchResult>;
156
+ /**
157
+ * Evaluate every rule against an event and return the rules that matched,
158
+ * with their reasons. Rules are independent: one throwing lookup does not
159
+ * suppress the others.
160
+ */
161
+ declare function matchRules(rules: readonly Rule[], event: AlertEvent, context?: EvaluationContext): Promise<{
162
+ rule: Rule;
163
+ result: MatchResult;
164
+ }[]>;
165
+
166
+ export { type DeployerReputation, type EvaluationContext, type MatchResult, type PriorLaunch, type ReputationProvider, type ReputationProviderOptions, Rule, createRpcReputationProvider, evaluateRule, matchRules, priorLaunchesBefore };
@@ -0,0 +1,166 @@
1
+ import { b as Rule } from '../schema-DuDMPqpq.js';
2
+ export { P as Platform, R as RateLimitConfig, a as ReputationFilter, c as RuleInput, S as Subscription, d as SubscriptionInput, e as addressSchema, f as canEmit, g as defaultRules, h as eventKindSchema, i as formatValidationError, l as launchpadSchema, p as parseRule, j as parseSubscription, k as platformSchema, r as rateLimitSchema, m as reputationFilterSchema, n as ruleComplexity, o as ruleSchema, s as safeParseRule, q as safeParseSubscription, t as sideSchema, u as subscriptionSchema, v as usesOnChainLookups } from '../schema-DuDMPqpq.js';
3
+ import { Address } from 'viem';
4
+ import { e as Launchpad, A as AlertEvent } from '../types-DFPpTMWo.js';
5
+ import { HoodClient } from 'hoodchain';
6
+ import { F as FetchLogRangeOptions } from '../logs-D7sYBvSy.js';
7
+ import { P as PriceOracle } from '../pricing-BumgLpvB.js';
8
+ export { R as RateLimitDecision, a as RateLimitStore, c as checkRateLimit, b as createMemoryRateLimitStore, r as rateLimitKey } from '../ratelimit-CGLlXNIS.js';
9
+ import 'zod';
10
+
11
+ /**
12
+ * Deployer reputation, derived entirely from chain state.
13
+ *
14
+ * Nothing here is scraped, self-reported or scored by a model. Three
15
+ * measurements, each with an exact on-chain definition:
16
+ *
17
+ * - **Prior launches**: `TokenLaunched` logs on the NOXA factory with
18
+ * `deployer` indexed to this address, plus `TokenCreated` logs on the three
19
+ * Odyssey factories with `creator` indexed to this address. Both queries are
20
+ * topic-selective, so the RPC serves them across the full chain history in
21
+ * one call each.
22
+ * - **LP locked**: for a NOXA launch, the LP position NFT is locked when
23
+ * `NonfungiblePositionManager.ownerOf(positionId)` is the NOXA locker
24
+ * contract. That is the launchpad's own permanent-lock mechanism, read
25
+ * directly rather than trusted from a UI badge.
26
+ * - **Rugged**: a prior launch whose pool quote-token reserve has since fallen
27
+ * below `rugThresholdUsd`. Liquidity that was seeded and is now gone is the
28
+ * observable footprint of a pull. It is a heuristic and is labelled as one
29
+ * everywhere it surfaces: a token that simply never traded and a token whose
30
+ * liquidity was withdrawn both end up with an empty pool, and only the
31
+ * second is a rug. Pair it with `minPriorLaunches` for a meaningful signal.
32
+ *
33
+ * Results are cached per deployer, because a launch burst from one deployer
34
+ * would otherwise re-run the same history scan per event.
35
+ */
36
+ /** One prior launch by a deployer. */
37
+ interface PriorLaunch {
38
+ launchpad: Launchpad;
39
+ token: Address;
40
+ pool: Address | null;
41
+ quoteToken: Address | null;
42
+ /** NOXA LP position id, when the launch had one. */
43
+ positionId: bigint | null;
44
+ blockNumber: bigint;
45
+ }
46
+ /** A deployer's on-chain track record. */
47
+ interface DeployerReputation {
48
+ deployer: Address;
49
+ /** Every launch found, oldest first. Capped by `maxHistory`. */
50
+ launches: PriorLaunch[];
51
+ /** Launches whose LP position is held by the NOXA locker. */
52
+ lockedLaunches: number;
53
+ /** Launches whose pool reserve is now below the rug threshold. */
54
+ ruggedLaunches: number;
55
+ /**
56
+ * How many launches were actually inspected for lock/rug status. Lower than
57
+ * `launches.length` when the history exceeds `maxInspect`, and surfaced so
58
+ * consumers never read a partial scan as a clean record.
59
+ */
60
+ inspected: number;
61
+ /** When this record was computed, ms since epoch. */
62
+ computedAtMs: number;
63
+ }
64
+ /** Reputation lookups for the rule engine. */
65
+ interface ReputationProvider {
66
+ get(deployer: Address): Promise<DeployerReputation>;
67
+ /** Is a specific LP position locked by the launchpad locker? */
68
+ isLpLocked(positionId: bigint): Promise<boolean>;
69
+ }
70
+ /** Options for {@link createRpcReputationProvider}. */
71
+ interface ReputationProviderOptions {
72
+ /** Cache lifetime per deployer, in ms. @defaultValue `600_000` (10 minutes) */
73
+ ttlMs?: number;
74
+ /** Max launches kept per deployer. @defaultValue `200` */
75
+ maxHistory?: number;
76
+ /**
77
+ * Max prior launches inspected for lock/rug status per deployer. Each one
78
+ * costs a pool balance read, so this bounds the RPC cost of a rule.
79
+ * @defaultValue `25`
80
+ */
81
+ maxInspect?: number;
82
+ /** Pool reserve below this USD value counts as drained. @defaultValue `50` */
83
+ rugThresholdUsd?: number;
84
+ /** Retry/bisect options for the history queries. */
85
+ logOptions?: FetchLogRangeOptions;
86
+ /** Clock injection point for tests. @defaultValue `Date.now` */
87
+ now?: () => number;
88
+ }
89
+ /**
90
+ * Build an RPC-backed reputation provider.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const reputation = createRpcReputationProvider(hood, oracle)
95
+ * const record = await reputation.get('0xDeployer…')
96
+ * console.log(record.launches.length, 'prior launches,', record.ruggedLaunches, 'drained')
97
+ * ```
98
+ */
99
+ declare function createRpcReputationProvider(client: HoodClient, oracle: PriceOracle, options?: ReputationProviderOptions): ReputationProvider;
100
+ /**
101
+ * Count a deployer's launches that happened strictly before a block. Rules ask
102
+ * "how many had they launched *at the time of this event*", which is not the
103
+ * same as "how many have they launched now", and only the first is stable when
104
+ * a historical range is re-processed after a restart.
105
+ */
106
+ declare function priorLaunchesBefore(record: DeployerReputation, blockNumber: bigint): number;
107
+
108
+ /**
109
+ * The rule engine: does this event satisfy this rule?
110
+ *
111
+ * Two properties matter more than the filter list itself.
112
+ *
113
+ * **Lazy, ordered evaluation.** Cheap in-memory filters (kind, launchpad,
114
+ * watchlists, USD bounds) run before anything that costs an RPC call
115
+ * (liquidity, reputation). A rule that rejects an event on its kind never
116
+ * triggers a pool balance read. With thousands of subscriptions this is the
117
+ * difference between a service that keeps up with the chain and one that does
118
+ * not.
119
+ *
120
+ * **Unknown is not a pass.** Every USD and liquidity figure can legitimately
121
+ * be `null` (no USDG or WETH leg to price against, an unreadable pool). A
122
+ * filter that asks for a measurement the chain did not give us does not match.
123
+ * The alternative, treating unknown as zero or as infinity, silently satisfies
124
+ * either `minUsd` or `maxLiquidityUsd` and ships alerts that are simply wrong.
125
+ */
126
+ /** Lazily-invoked lookups the engine uses only when a rule needs them. */
127
+ interface EvaluationContext {
128
+ /** USD liquidity of the event token's deepest known pool. */
129
+ liquidityUsd?: (event: AlertEvent) => Promise<number | null>;
130
+ /** On-chain track record for a deployer. */
131
+ reputation?: (deployer: Address) => Promise<DeployerReputation>;
132
+ /** Is this LP position held by the launchpad locker? */
133
+ isLpLocked?: (positionId: bigint) => Promise<boolean>;
134
+ }
135
+ /** The outcome of evaluating one rule against one event. */
136
+ interface MatchResult {
137
+ matched: boolean;
138
+ /**
139
+ * Why. On a match this names the rule; on a miss it names the first filter
140
+ * that rejected the event. The bot's `/status` and the service's debug log
141
+ * both surface this, so "why didn't I get an alert" is answerable.
142
+ */
143
+ reason: string;
144
+ }
145
+ /**
146
+ * Evaluate a rule against an event.
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * const rule = parseRule({ id: 'whales', kinds: ['whale_trade'], minUsd: 5000 })
151
+ * const result = await evaluateRule(rule, event)
152
+ * if (!result.matched) console.log('skipped:', result.reason)
153
+ * ```
154
+ */
155
+ declare function evaluateRule(rule: Rule, event: AlertEvent, context?: EvaluationContext): Promise<MatchResult>;
156
+ /**
157
+ * Evaluate every rule against an event and return the rules that matched,
158
+ * with their reasons. Rules are independent: one throwing lookup does not
159
+ * suppress the others.
160
+ */
161
+ declare function matchRules(rules: readonly Rule[], event: AlertEvent, context?: EvaluationContext): Promise<{
162
+ rule: Rule;
163
+ result: MatchResult;
164
+ }[]>;
165
+
166
+ export { type DeployerReputation, type EvaluationContext, type MatchResult, type PriorLaunch, type ReputationProvider, type ReputationProviderOptions, Rule, createRpcReputationProvider, evaluateRule, matchRules, priorLaunchesBefore };
@@ -0,0 +1,60 @@
1
+ import "../chunk-3NHLSKZE.js";
2
+ import {
3
+ checkRateLimit,
4
+ createMemoryRateLimitStore,
5
+ createRpcReputationProvider,
6
+ evaluateRule,
7
+ matchRules,
8
+ priorLaunchesBefore,
9
+ rateLimitKey
10
+ } from "../chunk-Y2M3USWZ.js";
11
+ import "../chunk-6JQK5G3Z.js";
12
+ import {
13
+ addressSchema,
14
+ canEmit,
15
+ defaultRules,
16
+ eventKindSchema,
17
+ formatValidationError,
18
+ launchpadSchema,
19
+ parseRule,
20
+ parseSubscription,
21
+ platformSchema,
22
+ rateLimitSchema,
23
+ reputationFilterSchema,
24
+ ruleComplexity,
25
+ ruleSchema,
26
+ safeParseRule,
27
+ safeParseSubscription,
28
+ sideSchema,
29
+ subscriptionSchema,
30
+ usesOnChainLookups
31
+ } from "../chunk-VC5MKEY2.js";
32
+ import "../chunk-3LHCQA4Z.js";
33
+ export {
34
+ addressSchema,
35
+ canEmit,
36
+ checkRateLimit,
37
+ createMemoryRateLimitStore,
38
+ createRpcReputationProvider,
39
+ defaultRules,
40
+ evaluateRule,
41
+ eventKindSchema,
42
+ formatValidationError,
43
+ launchpadSchema,
44
+ matchRules,
45
+ parseRule,
46
+ parseSubscription,
47
+ platformSchema,
48
+ priorLaunchesBefore,
49
+ rateLimitKey,
50
+ rateLimitSchema,
51
+ reputationFilterSchema,
52
+ ruleComplexity,
53
+ ruleSchema,
54
+ safeParseRule,
55
+ safeParseSubscription,
56
+ sideSchema,
57
+ subscriptionSchema,
58
+ usesOnChainLookups
59
+ };
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}