1import { Actor, log } from 'apify';
2
3await Actor.init();
4
5const input = await Actor.getInput() ?? {};
6const {
7 languages = ['TypeScript', 'JavaScript', 'Python'],
8 maxComments = 5,
9 minBountyUsd = 30,
10 maxAgeDays = 180,
11 maxItems = 25,
12 excludeHoneypots = true,
13 githubToken = '',
14} = input;
15
16const HONEYPOT_PATTERNS = [
17 /startup configuration/i,
18 /paste without modification/i,
19 /instructions loaded into your context/i,
20 /home directory,\s*working directory/i,
21 /@generated-by/i,
22];
23
24
25const MIRROR_SPAM_REPOS = [
26 'bounty-plaza',
27 'bounty-watcher',
28 'sn-monetization-runtime',
29];
30
31const headers = {
32 'Accept': 'application/vnd.github+json',
33 'User-Agent': 'Apify-Verified-Bounty-Sniper/1.0',
34};
35if (githubToken) {
36 headers['Authorization'] = `Bearer ${githubToken}`;
37}
38
39const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
40
41function extractBountyUsd(issue) {
42 const searchPool = [
43 issue.title || '',
44 ...(issue.labels || []).map((l) => l.name),
45 issue.body || '',
46 ].join(' ');
47
48 const match = searchPool.match(/\$\s?(\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s?(k)?/i);
49 if (!match) return null;
50
51 let amount = parseFloat(match[1].replace(/,/g, ''));
52 if (match[2] && match[2].toLowerCase() === 'k') {
53 amount *= 1000;
54 }
55 return amount;
56}
57
58function isHoneypot(issue) {
59 const body = issue.body || '';
60 return HONEYPOT_PATTERNS.some((regex) => regex.test(body));
61}
62
63const cutoffDate = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1000)
64 .toISOString()
65 .split('T')[0];
66
67const githubMaxComments = maxComments + 1;
68const seenUrls = new Set();
69const repoCounts = {};
70let totalSaved = 0;
71
72for (const lang of languages) {
73 if (totalSaved >= maxItems) break;
74
75
76
77 const queries = [
78 `is:issue is:open "algora.io" language:${lang} comments:<=${githubMaxComments} created:>=${cutoffDate} sort:created-desc`,
79 `is:issue is:open "opire.dev" language:${lang} comments:<=${githubMaxComments} created:>=${cutoffDate} sort:created-desc`,
80 `is:issue is:open label:bounty language:${lang} comments:<=${githubMaxComments} created:>=${cutoffDate} sort:created-desc`,
81 ];
82
83 for (const query of queries) {
84 if (totalSaved >= maxItems) break;
85
86 const url = `https://api.github.com/search/issues?q=${encodeURIComponent(query)}&per_page=30`;
87 log.info(`Searching ${lang}: ${query}`);
88
89 const response = await fetch(url, { headers });
90 if (!response.ok) {
91 log.warning(`GitHub API status ${response.status} (rate limit). Waiting 3s...`);
92 await sleep(3000);
93 continue;
94 }
95
96 const data = await response.json();
97 const issues = data.items || [];
98
99 for (const issue of issues) {
100 if (totalSaved >= maxItems) break;
101 if (seenUrls.has(issue.html_url)) continue;
102
103 const repoFullName = issue.repository_url.split('/repos/')[1] || '';
104
105
106 if (MIRROR_SPAM_REPOS.some((spam) => repoFullName.toLowerCase().includes(spam))) {
107 continue;
108 }
109 if (excludeHoneypots && (isHoneypot(issue) || issue.user?.type === 'Bot')) {
110 log.warning(`Blocked bot/honeypot issue: ${issue.html_url}`);
111 continue;
112 }
113
114
115 repoCounts[repoFullName] = (repoCounts[repoFullName] || 0) + 1;
116 if (repoCounts[repoFullName] > 3) continue;
117
118 const humanComments = Math.max(0, (issue.comments || 0) - 1);
119 if (humanComments > maxComments) continue;
120
121 const estimatedUsd = extractBountyUsd(issue);
122 const labelNames = (issue.labels || []).map((l) => l.name);
123 const hasDiamondLabel = labelNames.some((n) => n.includes('💎'));
124 const bodyText = issue.body || '';
125 const isEscrowVerified =
126 hasDiamondLabel ||
127 bodyText.includes('algora.io') ||
128 bodyText.includes('opire.dev') ||
129 query.includes('"algora.io"') ||
130 query.includes('"opire.dev"');
131
132 if (!estimatedUsd && !isEscrowVerified) continue;
133 if (estimatedUsd && estimatedUsd < minBountyUsd) continue;
134
135 seenUrls.add(issue.html_url);
136
137 const bountyRecord = {
138 title: issue.title,
139 estimatedRewardUsd: estimatedUsd ?? minBountyUsd,
140 language: lang,
141 humanCommentsCount: humanComments,
142 totalCommentsCount: issue.comments,
143 repository: repoFullName,
144 issueNumber: issue.number,
145 issueUrl: issue.html_url,
146 author: issue.user?.login,
147 createdAt: issue.created_at,
148 escrowVerified: isEscrowVerified,
149 labels: labelNames,
150 bodyPreview: bodyText.slice(0, 300).replace(/[^\x20-\x7E]/g, ' ').trim(),
151 };
152
153 const chargeResult = await Actor.pushData(bountyRecord);
154 totalSaved++;
155
156 if (chargeResult?.eventChargeLimitReached) {
157 log.info('User maximum budget reached. Stopping run cleanly.');
158 await Actor.exit();
159 }
160 }
161
162
163 if (!githubToken) {
164 await sleep(2000);
165 }
166 }
167}
168
169log.info(`Done! Extracted ${totalSaved} diverse, verified bounties.`);
170await Actor.exit();