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,471 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/tiers/index.ts
21
+ var tiers_exports = {};
22
+ __export(tiers_exports, {
23
+ TIER_POLICIES: () => TIER_POLICIES,
24
+ accrueExpiry: () => accrueExpiry,
25
+ canAddRule: () => canAddRule,
26
+ canAddSubscription: () => canAddSubscription,
27
+ chainEntitlementProviders: () => chainEntitlementProviders,
28
+ createEntitlementGate: () => createEntitlementGate,
29
+ createStaticEntitlementProvider: () => createStaticEntitlementProvider,
30
+ createUsdgEntitlementProvider: () => createUsdgEntitlementProvider,
31
+ isRuleAllowed: () => isRuleAllowed,
32
+ linkMessage: () => linkMessage,
33
+ policyFor: () => policyFor,
34
+ verifyWalletLink: () => verifyWalletLink
35
+ });
36
+ module.exports = __toCommonJS(tiers_exports);
37
+
38
+ // src/rules/schema.ts
39
+ var import_zod = require("zod");
40
+
41
+ // src/events/types.ts
42
+ var LAUNCHPAD_EVENT_KINDS = {
43
+ noxa: ["launch", "whale_trade"],
44
+ odyssey: ["launch", "curve_trade", "graduation", "whale_trade"]
45
+ };
46
+
47
+ // src/rules/schema.ts
48
+ var addressPattern = /^0x[0-9a-fA-F]{40}$/;
49
+ var addressSchema = import_zod.z.string().regex(addressPattern, "must be a 0x-prefixed 20-byte address").transform((value) => value.toLowerCase());
50
+ var eventKindSchema = import_zod.z.enum(["launch", "curve_trade", "graduation", "whale_trade"]);
51
+ var launchpadSchema = import_zod.z.enum(["noxa", "odyssey"]);
52
+ var sideSchema = import_zod.z.enum(["buy", "sell", "any"]);
53
+ var reputationFilterSchema = import_zod.z.object({
54
+ /** Require the deployer to have launched at least this many tokens before. */
55
+ minPriorLaunches: import_zod.z.number().int().min(0).max(1e4).optional(),
56
+ /** Reject serial deployers above this prior-launch count. */
57
+ maxPriorLaunches: import_zod.z.number().int().min(0).max(1e4).optional(),
58
+ /**
59
+ * Reject a deployer with more than this many prior launches whose pool
60
+ * liquidity has since been drained below the rug threshold.
61
+ */
62
+ maxRuggedLaunches: import_zod.z.number().int().min(0).max(1e4).optional(),
63
+ /**
64
+ * Require this launch's own LP position to be held by the launchpad's
65
+ * locker contract. Only meaningful for `launch` events that carry a pool
66
+ * (NOXA); events without a pool never satisfy it.
67
+ */
68
+ requireLpLocked: import_zod.z.boolean().optional()
69
+ }).strict().refine(
70
+ (value) => value.minPriorLaunches === void 0 || value.maxPriorLaunches === void 0 || value.minPriorLaunches <= value.maxPriorLaunches,
71
+ { message: "minPriorLaunches must not exceed maxPriorLaunches" }
72
+ );
73
+ var rateLimitSchema = import_zod.z.object({
74
+ /** Hard cap on alerts this rule may deliver per rolling hour. */
75
+ maxPerHour: import_zod.z.number().int().min(1).max(1e4).optional(),
76
+ /** Minimum gap between two alerts from this rule, in seconds. */
77
+ minIntervalSeconds: import_zod.z.number().int().min(1).max(86400).optional()
78
+ }).strict();
79
+ var ruleSchema = import_zod.z.object({
80
+ /** Stable id, unique within a subscription. Used in bot commands. */
81
+ id: import_zod.z.string().min(1).max(32).regex(/^[a-z0-9][a-z0-9_-]*$/, "must be lowercase letters, digits, hyphen or underscore"),
82
+ /** Human label shown in `/rules`. */
83
+ name: import_zod.z.string().min(1).max(64).optional(),
84
+ enabled: import_zod.z.boolean().default(true),
85
+ /** Event kinds this rule reacts to. Defaults to all four. */
86
+ kinds: import_zod.z.array(eventKindSchema).min(1).max(4).default(["launch", "curve_trade", "graduation", "whale_trade"]),
87
+ /** Launchpads this rule reacts to. Defaults to both. */
88
+ launchpads: import_zod.z.array(launchpadSchema).min(1).max(2).default(["noxa", "odyssey"]),
89
+ /** Minimum USD value of the event. Events with an unknown value never pass. */
90
+ minUsd: import_zod.z.number().min(0).max(1e12).optional(),
91
+ /** Maximum USD value of the event. */
92
+ maxUsd: import_zod.z.number().min(0).max(1e12).optional(),
93
+ /** Minimum USD liquidity of the token's deepest known pool. */
94
+ minLiquidityUsd: import_zod.z.number().min(0).max(1e12).optional(),
95
+ /** Maximum USD liquidity of the token's deepest known pool. */
96
+ maxLiquidityUsd: import_zod.z.number().min(0).max(1e12).optional(),
97
+ /** Watchlist. When non-empty, only these tokens match. */
98
+ tokens: import_zod.z.array(addressSchema).max(500).default([]),
99
+ /** Deployer watchlist. When non-empty, only these actors match. */
100
+ deployers: import_zod.z.array(addressSchema).max(500).default([]),
101
+ /** Deployers to always reject, applied after every other filter. */
102
+ excludeDeployers: import_zod.z.array(addressSchema).max(500).default([]),
103
+ /** Trade direction. Ignored by `launch` and `graduation` events. */
104
+ side: sideSchema.default("any"),
105
+ reputation: reputationFilterSchema.optional(),
106
+ rateLimit: rateLimitSchema.optional()
107
+ }).strict().refine((rule) => rule.minUsd === void 0 || rule.maxUsd === void 0 || rule.minUsd <= rule.maxUsd, {
108
+ message: "minUsd must not exceed maxUsd",
109
+ path: ["minUsd"]
110
+ }).refine(
111
+ (rule) => rule.minLiquidityUsd === void 0 || rule.maxLiquidityUsd === void 0 || rule.minLiquidityUsd <= rule.maxLiquidityUsd,
112
+ { message: "minLiquidityUsd must not exceed maxLiquidityUsd", path: ["minLiquidityUsd"] }
113
+ ).refine((rule) => rule.kinds.some((kind) => rule.launchpads.some((pad) => canEmit(pad, kind))), {
114
+ message: "this combination can never fire: NOXA is an instant launcher with no bonding curve, so it emits no curve_trade and no graduation",
115
+ path: ["kinds"]
116
+ });
117
+ var platformSchema = import_zod.z.enum(["telegram", "discord"]);
118
+ var subscriptionSchema = import_zod.z.object({
119
+ id: import_zod.z.string().min(1).max(64),
120
+ /**
121
+ * The account that owns this subscription, and the unit entitlements are
122
+ * checked against. For Telegram this is the user id, for Discord the guild
123
+ * id (a server pays once for its channels) or user id for a DM.
124
+ */
125
+ subscriberId: import_zod.z.string().min(1).max(128),
126
+ platform: platformSchema,
127
+ /**
128
+ * Where alerts go: a Telegram chat id, a Discord channel id, or a Discord
129
+ * webhook URL. Validated per platform by the notifier, not here, because
130
+ * the same subscription record serves all three shapes.
131
+ */
132
+ target: import_zod.z.string().min(1).max(512),
133
+ enabled: import_zod.z.boolean().default(true),
134
+ rules: import_zod.z.array(ruleSchema).max(100).default([]),
135
+ createdAtMs: import_zod.z.number().int().nonnegative()
136
+ }).strict().refine((sub) => new Set(sub.rules.map((rule) => rule.id)).size === sub.rules.length, {
137
+ message: "rule ids must be unique within a subscription",
138
+ path: ["rules"]
139
+ });
140
+ function canEmit(launchpad, kind) {
141
+ return LAUNCHPAD_EVENT_KINDS[launchpad].includes(kind);
142
+ }
143
+ function ruleComplexity(rule) {
144
+ let score = 0;
145
+ if (rule.minUsd !== void 0) score += 1;
146
+ if (rule.maxUsd !== void 0) score += 1;
147
+ if (rule.minLiquidityUsd !== void 0) score += 1;
148
+ if (rule.maxLiquidityUsd !== void 0) score += 1;
149
+ if (rule.tokens.length > 0) score += 1;
150
+ if (rule.deployers.length > 0) score += 1;
151
+ if (rule.excludeDeployers.length > 0) score += 1;
152
+ if (rule.side !== "any") score += 1;
153
+ if (rule.reputation) score += Object.values(rule.reputation).filter((v) => v !== void 0).length;
154
+ if (rule.rateLimit) score += 1;
155
+ return score;
156
+ }
157
+ function usesOnChainLookups(rule) {
158
+ return rule.minLiquidityUsd !== void 0 || rule.maxLiquidityUsd !== void 0 || rule.reputation !== void 0 && Object.values(rule.reputation).some((value) => value !== void 0);
159
+ }
160
+
161
+ // src/tiers/policy.ts
162
+ var TIER_POLICIES = {
163
+ free: {
164
+ tier: "free",
165
+ maxSubscriptions: 1,
166
+ maxRulesPerSubscription: 3,
167
+ maxRuleComplexity: 3,
168
+ maxWatchlistTokens: 5,
169
+ maxAlertsPerHour: 30,
170
+ deliveryDelayMs: 6e4,
171
+ allowOnChainLookupFilters: false,
172
+ allowedKinds: ["launch", "graduation", "whale_trade"]
173
+ },
174
+ premium: {
175
+ tier: "premium",
176
+ maxSubscriptions: 10,
177
+ maxRulesPerSubscription: 50,
178
+ maxRuleComplexity: 24,
179
+ maxWatchlistTokens: 500,
180
+ maxAlertsPerHour: 2e3,
181
+ deliveryDelayMs: 0,
182
+ allowOnChainLookupFilters: true,
183
+ allowedKinds: ["launch", "curve_trade", "graduation", "whale_trade"]
184
+ }
185
+ };
186
+ var OK = { allowed: true, reason: null };
187
+ var deny = (reason) => ({ allowed: false, reason });
188
+ function canAddSubscription(policy, currentCount) {
189
+ if (currentCount >= policy.maxSubscriptions) {
190
+ return deny(
191
+ `the ${policy.tier} tier allows ${policy.maxSubscriptions} subscription${policy.maxSubscriptions === 1 ? "" : "s"} and you have ${currentCount}. Upgrade with /upgrade, or remove one with /unsubscribe.`
192
+ );
193
+ }
194
+ return OK;
195
+ }
196
+ function canAddRule(policy, rule, currentRuleCount) {
197
+ if (currentRuleCount >= policy.maxRulesPerSubscription) {
198
+ return deny(
199
+ `the ${policy.tier} tier allows ${policy.maxRulesPerSubscription} rules per subscription and you have ${currentRuleCount}.`
200
+ );
201
+ }
202
+ return isRuleAllowed(policy, rule);
203
+ }
204
+ function isRuleAllowed(policy, rule) {
205
+ const disallowedKinds = rule.kinds.filter((kind) => !policy.allowedKinds.includes(kind));
206
+ if (disallowedKinds.length > 0) {
207
+ return deny(
208
+ `the ${policy.tier} tier does not include ${disallowedKinds.join(", ")} alerts. Allowed: ${policy.allowedKinds.join(", ")}.`
209
+ );
210
+ }
211
+ const complexity = ruleComplexity(rule);
212
+ if (complexity > policy.maxRuleComplexity) {
213
+ return deny(
214
+ `this rule uses ${complexity} filters and the ${policy.tier} tier allows ${policy.maxRuleComplexity}.`
215
+ );
216
+ }
217
+ if (rule.tokens.length > policy.maxWatchlistTokens) {
218
+ return deny(
219
+ `the ${policy.tier} tier allows ${policy.maxWatchlistTokens} watchlist tokens and this rule has ${rule.tokens.length}.`
220
+ );
221
+ }
222
+ if (rule.deployers.length + rule.excludeDeployers.length > policy.maxWatchlistTokens) {
223
+ return deny(
224
+ `the ${policy.tier} tier allows ${policy.maxWatchlistTokens} watched deployers per rule.`
225
+ );
226
+ }
227
+ if (!policy.allowOnChainLookupFilters && usesOnChainLookups(rule)) {
228
+ return deny(
229
+ `liquidity and deployer-reputation filters are a premium feature: each one costs an on-chain read per candidate event.`
230
+ );
231
+ }
232
+ return OK;
233
+ }
234
+ function policyFor(tier, overrides) {
235
+ const base = TIER_POLICIES[tier];
236
+ const override = overrides?.[tier];
237
+ return override ? { ...base, ...override, tier } : base;
238
+ }
239
+
240
+ // src/tiers/entitlements.ts
241
+ var import_viem = require("viem");
242
+ var import_hoodchain = require("hoodchain");
243
+ var import_viem2 = require("viem");
244
+
245
+ // src/events/logs.ts
246
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
247
+ function isResultTooLarge(error) {
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ return /exceeds limit|too many results|response size exceeded|query returned more than/i.test(message);
250
+ }
251
+ function isRetryableRpcError(error) {
252
+ const message = error instanceof Error ? error.message : String(error);
253
+ return /too many requests|rate limit|timed out|timeout|429|503|502|ECONNRESET|socket hang up|fetch failed/i.test(
254
+ message
255
+ );
256
+ }
257
+ async function fetchLogRange(fetcher, from, to, options = {}) {
258
+ const minSpan = options.minSpan ?? 1n;
259
+ const retries = options.retries ?? 5;
260
+ const backoffMs = options.backoffMs ?? 500;
261
+ const sleep = options.sleep ?? defaultSleep;
262
+ const run = async (lo, hi) => {
263
+ let attempt = 0;
264
+ for (; ; ) {
265
+ try {
266
+ return [...await fetcher(lo, hi)];
267
+ } catch (error) {
268
+ if (isResultTooLarge(error)) {
269
+ const span = hi - lo;
270
+ if (span < minSpan || span === 0n) throw error;
271
+ const mid = lo + span / 2n;
272
+ const left = await run(lo, mid);
273
+ const right = await run(mid + 1n, hi);
274
+ return [...left, ...right];
275
+ }
276
+ if (isRetryableRpcError(error) && attempt < retries) {
277
+ await sleep(backoffMs * 2 ** attempt);
278
+ attempt += 1;
279
+ continue;
280
+ }
281
+ throw error;
282
+ }
283
+ }
284
+ };
285
+ if (to < from) return [];
286
+ return run(from, to);
287
+ }
288
+
289
+ // src/tiers/entitlements.ts
290
+ var free = (subscriberId, source) => ({
291
+ subscriberId,
292
+ tier: "free",
293
+ expiresAtMs: null,
294
+ source
295
+ });
296
+ function createStaticEntitlementProvider(options) {
297
+ const premium = new Set((options.premiumSubscribers ?? []).map((id) => id.trim()).filter(Boolean));
298
+ return {
299
+ async get(subscriberId) {
300
+ if (options.allPremium === true || premium.has(subscriberId)) {
301
+ return { subscriberId, tier: "premium", expiresAtMs: null, source: "static" };
302
+ }
303
+ return free(subscriberId, "static");
304
+ }
305
+ };
306
+ }
307
+ var transferEvent = {
308
+ type: "event",
309
+ name: "Transfer",
310
+ inputs: [
311
+ { name: "from", type: "address", indexed: true },
312
+ { name: "to", type: "address", indexed: true },
313
+ { name: "value", type: "uint256", indexed: false }
314
+ ]
315
+ };
316
+ function accrueExpiry(payments, pricePerPeriodUsdg, periodDays) {
317
+ if (pricePerPeriodUsdg <= 0) return null;
318
+ const periodMs = periodDays * 864e5;
319
+ const ordered = [...payments].sort((a, b) => a.atMs - b.atMs);
320
+ let expiry = null;
321
+ for (const payment of ordered) {
322
+ const periods = Math.floor(payment.amountUsdg / pricePerPeriodUsdg);
323
+ if (periods < 1) continue;
324
+ const start = expiry !== null && expiry > payment.atMs ? expiry : payment.atMs;
325
+ expiry = start + periods * periodMs;
326
+ }
327
+ return expiry;
328
+ }
329
+ function createUsdgEntitlementProvider(options) {
330
+ const ttlMs = options.ttlMs ?? 3e5;
331
+ const now = options.now ?? Date.now;
332
+ const periodDays = options.periodDays ?? 30;
333
+ const usdg = options.client.network === "testnet" ? "0x7E955252E15c84f5768B83c41a71F9eba181802F" : "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
334
+ const cache = /* @__PURE__ */ new Map();
335
+ async function resolve(subscriberId) {
336
+ const wallet = await options.links.walletOf(subscriberId);
337
+ if (!wallet) return free(subscriberId, "usdg:no-linked-wallet");
338
+ const head = await options.client.public.getBlockNumber();
339
+ const logs = await fetchLogRange(
340
+ (from, to) => options.client.public.getLogs({
341
+ address: usdg,
342
+ event: transferEvent,
343
+ args: { from: wallet, to: (0, import_viem.getAddress)(options.receiver) },
344
+ fromBlock: from,
345
+ toBlock: to
346
+ }),
347
+ options.fromBlock ?? 0n,
348
+ head,
349
+ options.logOptions ?? {}
350
+ );
351
+ if (logs.length === 0) return free(subscriberId, "usdg:no-payments");
352
+ const payments = [];
353
+ for (const log of logs) {
354
+ const value = log.args.value;
355
+ if (value === void 0 || value === 0n) continue;
356
+ const block = await options.client.public.getBlock({
357
+ blockNumber: log.blockNumber,
358
+ includeTransactions: false
359
+ });
360
+ payments.push({
361
+ atMs: Number(block.timestamp) * 1e3,
362
+ amountUsdg: Number((0, import_viem2.formatUnits)(value, import_hoodchain.USDG_DECIMALS))
363
+ });
364
+ }
365
+ const expiresAtMs = accrueExpiry(payments, options.pricePerPeriodUsdg, periodDays);
366
+ if (expiresAtMs === null || expiresAtMs <= now()) {
367
+ return { subscriberId, tier: "free", expiresAtMs, source: "usdg:lapsed" };
368
+ }
369
+ return { subscriberId, tier: "premium", expiresAtMs, source: "usdg" };
370
+ }
371
+ return {
372
+ async get(subscriberId) {
373
+ const hit = cache.get(subscriberId);
374
+ if (hit && now() - hit.at < ttlMs) return hit.entitlement;
375
+ try {
376
+ const entitlement = await resolve(subscriberId);
377
+ cache.set(subscriberId, { entitlement, at: now() });
378
+ return entitlement;
379
+ } catch (error) {
380
+ if (hit) return hit.entitlement;
381
+ const message = error instanceof Error ? error.message : String(error);
382
+ return free(subscriberId, `usdg:unavailable (${message})`);
383
+ }
384
+ }
385
+ };
386
+ }
387
+ function chainEntitlementProviders(...providers) {
388
+ return {
389
+ async get(subscriberId) {
390
+ let last = free(subscriberId, "chain:empty");
391
+ for (const provider of providers) {
392
+ const entitlement = await provider.get(subscriberId);
393
+ if (entitlement.tier === "premium") return entitlement;
394
+ last = entitlement;
395
+ }
396
+ return last;
397
+ }
398
+ };
399
+ }
400
+ function linkMessage(subscriberId, nonce) {
401
+ return `hood-alerts wallet link
402
+ subscriber: ${subscriberId}
403
+ nonce: ${nonce}`;
404
+ }
405
+ async function verifyWalletLink(subscriberId, nonce, address, signature) {
406
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) return null;
407
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return null;
408
+ try {
409
+ const valid = await (0, import_viem.verifyMessage)({
410
+ address: (0, import_viem.getAddress)(address),
411
+ message: linkMessage(subscriberId, nonce),
412
+ signature
413
+ });
414
+ return valid ? (0, import_viem.getAddress)(address) : null;
415
+ } catch {
416
+ return null;
417
+ }
418
+ }
419
+
420
+ // src/tiers/enforce.ts
421
+ function createEntitlementGate(options) {
422
+ async function resolve(subscriberId) {
423
+ const entitlement = await options.provider.get(subscriberId);
424
+ return { entitlement, policy: policyFor(entitlement.tier, options.policyOverrides) };
425
+ }
426
+ return {
427
+ resolve,
428
+ async checkAddSubscription(subscriberId, currentCount) {
429
+ const { policy } = await resolve(subscriberId);
430
+ return canAddSubscription(policy, currentCount);
431
+ },
432
+ async checkAddRule(subscriberId, rule, currentRuleCount) {
433
+ const { policy } = await resolve(subscriberId);
434
+ return canAddRule(policy, rule, currentRuleCount);
435
+ },
436
+ async checkRule(subscriberId, rule) {
437
+ const { policy } = await resolve(subscriberId);
438
+ return isRuleAllowed(policy, rule);
439
+ },
440
+ async deliveryDelayMs(subscriberId) {
441
+ const { policy } = await resolve(subscriberId);
442
+ return policy.deliveryDelayMs;
443
+ },
444
+ async checkThroughput(subscriberId, deliveredInLastHour) {
445
+ const { policy } = await resolve(subscriberId);
446
+ if (deliveredInLastHour >= policy.maxAlertsPerHour) {
447
+ return {
448
+ allowed: false,
449
+ reason: `hourly alert cap reached (${policy.maxAlertsPerHour} on the ${policy.tier} tier)`
450
+ };
451
+ }
452
+ return { allowed: true, reason: null };
453
+ }
454
+ };
455
+ }
456
+ // Annotate the CommonJS export names for ESM import in node:
457
+ 0 && (module.exports = {
458
+ TIER_POLICIES,
459
+ accrueExpiry,
460
+ canAddRule,
461
+ canAddSubscription,
462
+ chainEntitlementProviders,
463
+ createEntitlementGate,
464
+ createStaticEntitlementProvider,
465
+ createUsdgEntitlementProvider,
466
+ isRuleAllowed,
467
+ linkMessage,
468
+ policyFor,
469
+ verifyWalletLink
470
+ });
471
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tiers/index.ts","../../src/rules/schema.ts","../../src/events/types.ts","../../src/tiers/policy.ts","../../src/tiers/entitlements.ts","../../src/events/logs.ts","../../src/tiers/enforce.ts"],"sourcesContent":["/**\n * Tiering and entitlements: what free and premium may do, and who is premium.\n *\n * @packageDocumentation\n */\n\nexport {\n TIER_POLICIES,\n canAddRule,\n canAddSubscription,\n isRuleAllowed,\n policyFor,\n} from './policy.js'\nexport type { PolicyDecision, Tier, TierPolicy } from './policy.js'\n\nexport {\n accrueExpiry,\n chainEntitlementProviders,\n createStaticEntitlementProvider,\n createUsdgEntitlementProvider,\n linkMessage,\n verifyWalletLink,\n} from './entitlements.js'\nexport type {\n Entitlement,\n EntitlementProvider,\n UsdgEntitlementOptions,\n WalletLinkStore,\n} from './entitlements.js'\n\nexport { createEntitlementGate } from './enforce.js'\nexport type { EntitlementGate, EntitlementGateOptions } from './enforce.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 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 { 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","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 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"],"mappings":"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","names":["import_viem"]}