From 44eb7e1d467354dbf03c43445ddf2009883f2e65 Mon Sep 17 00:00:00 2001 From: peterxing <9293096+peterxing@users.noreply.github.com> Date: Sat, 11 Jul 2026 04:18:29 +1000 Subject: [PATCH 1/3] Add Algora bounty discovery fallback --- docs/algora-opportunity-scout-proof.md | 43 ++++ .../runtime-opportunity-scout/index.ts | 208 +++++++++++++++--- 2 files changed, 222 insertions(+), 29 deletions(-) create mode 100644 docs/algora-opportunity-scout-proof.md diff --git a/docs/algora-opportunity-scout-proof.md b/docs/algora-opportunity-scout-proof.md new file mode 100644 index 00000000000..42bafaf493b --- /dev/null +++ b/docs/algora-opportunity-scout-proof.md @@ -0,0 +1,43 @@ +# Algora Opportunity Scout Proof + +Checked on 2026-07-10 UTC for issue #4. + +## Documented API Probe + +Request: + +```text +GET https://algora.io/api/bounties?status=open&limit=50 +Accept: application/json +``` + +Observed public response shape from the current endpoint was the interactive Algora HTML shell rather than JSON: + +```text +Bounties +Create new bounties by commenting /bounty $1000 on GitHub issues. +No open bounties +Create bounties by commenting /bounty $1000 on GitHub issues +``` + +The scout still calls this API first and will parse JSON if Algora returns `application/json`. + +## Public Fallback Samples + +When the API returns HTML, the scout reads public Algora bounty pages and extracts only visible real dollar amounts: + +```text +https://algora.io/SCIBASE.AI/bounties?status=open +$400 SCIBASE.AI#13 AI-Assisted Research Tools (MVP Level) + +https://algora.io/unsiloed-ai/bounties?status=open +$1,000 Unsiloed-chunker#34 Create an agentic RAG retrieval system + +https://algora.io/archestra-ai/bounties?status=open +$100 archestra#3859 json in mcp server args textarea + +https://algora.io/arakoodev/bounties?status=open +$50 EdgeChains#290 BOUNTY: integrate AWS Comprehend as a utility to redact data +``` + +Rewards are inserted as extracted numeric USD values, or `null` when no value can be parsed. diff --git a/supabase/functions/runtime-opportunity-scout/index.ts b/supabase/functions/runtime-opportunity-scout/index.ts index 340b0928d4a..d515067f25c 100644 --- a/supabase/functions/runtime-opportunity-scout/index.ts +++ b/supabase/functions/runtime-opportunity-scout/index.ts @@ -25,6 +25,7 @@ type Opportunity = { title: string; url: string; reward_usd: number | null; + tech_stack: string[]; raw: Record; }; @@ -77,6 +78,22 @@ function extractUsd(text: string): number | null { return null; } +function inferTechStack(text: string): string[] { + const haystack = text.toLowerCase(); + const tags: string[] = []; + const add = (tag: string, pattern: RegExp) => { + if (pattern.test(haystack) && !tags.includes(tag)) tags.push(tag); + }; + add("typescript", /\b(ts|typescript|javascript|sdk|react|node|npm|vite)\b/); + add("python", /\b(python|py|django|fastapi|jupyter)\b/); + add("ai", /\b(ai|agent|rag|llm|model|embedding|summari[sz]ation)\b/); + add("mcp", /\bmcp\b/); + add("database", /\b(qdrant|postgres|sqlite|chroma|vector|database|sql)\b/); + add("aws", /\b(aws|comprehend|s3|lambda)\b/); + add("docs", /\b(docs?|documentation|reference|content)\b/); + return tags; +} + // priority scales with reward; unknown reward gets a small baseline. function rewardPriority(reward: number | null): number { if (reward == null) return 3; @@ -132,6 +149,7 @@ async function fetchGitcoin(): Promise { title: name, url, reward_usd: reward, + tech_stack: inferTechStack(name), raw: { round_id: id, chain_id: chainId, @@ -184,6 +202,7 @@ async function fetchGithubBounties(): Promise { title, url: html, reward_usd: reward, + tech_stack: inferTechStack(`${repo} ${title} ${body}`), raw: { repo, number: Number(it.number ?? 0), @@ -199,45 +218,175 @@ async function fetchGithubBounties(): Promise { return out; } -// --- Source 3: Algora public bounties (best-effort, no key) --- -// If the public endpoint is unreachable or its shape changes, ignore cleanly. -async function fetchAlgora(): Promise { - const r = await fetchT("https://console.algora.io/api/bounties?status=open&limit=30", { - headers: { Accept: "application/json" }, - }); - if (!r.ok) return []; - const j = await r.json().catch(() => null); - // Tolerate both {items:[...]} and bare-array shapes. - const items: Array> = Array.isArray(j) - ? j - : (j?.items as Array>) || (j?.bounties as Array>) || []; - const out: Opportunity[] = []; - for (const it of items) { - const url = String(it.url || it.html_url || it.link || ""); - if (!url || !/^https?:\/\//.test(url)) continue; - const title = String(it.title || it.task || it.name || "").slice(0, 200); - // Algora amounts are typically minor units (cents) under reward/amount. - const rewardObj = (it.reward as Record | null) || (it.amount as Record | null) || null; - let reward: number | null = null; - if (rewardObj && typeof rewardObj === "object" && "amount" in rewardObj) { - const cents = Number((rewardObj as Record).amount ?? 0); - if (Number.isFinite(cents) && cents > 0) reward = cents / 100; - } else if (typeof it.amount_usd === "number") { - reward = it.amount_usd as number; - } else { - reward = extractUsd(title); +type AlgoraApiBounty = { + title?: unknown; + url?: unknown; + html_url?: unknown; + link?: unknown; + reward?: unknown; + amount?: unknown; + amount_usd?: unknown; + reward_usd?: unknown; + reward_formatted?: unknown; + status?: unknown; + org?: unknown; + organization?: unknown; + task?: unknown; +}; + +const ALGORA_API_URL = "https://algora.io/api/bounties?status=open&limit=50"; +const ALGORA_FALLBACK_ORGS = [ + "PrimeIntellect-ai", + "SCIBASE.AI", + "unsiloed-ai", + "daytonaio", + "archestra-ai", + "arakoodev", + "tscircuit", + "triggerdotdev", +]; + +function extractAlgoraReward(value: unknown, fallbackText = ""): number | null { + if (typeof value === "number" && Number.isFinite(value) && value > 0) return value; + if (typeof value === "string") return extractUsd(value); + if (value && typeof value === "object") { + const obj = value as Record; + const direct = extractAlgoraReward(obj.amount ?? obj.value ?? obj.usd ?? obj.cents, fallbackText); + if (direct != null) { + const currency = String(obj.currency ?? obj.currency_code ?? "USD").toUpperCase(); + const units = String(obj.units ?? "").toLowerCase(); + return currency === "USD" && (units === "cents" || direct >= 1000) ? direct / 100 : direct; } + } + return extractUsd(fallbackText); +} + +function parseAlgoraApiItems(payload: unknown): AlgoraApiBounty[] { + if (Array.isArray(payload)) return payload as AlgoraApiBounty[]; + if (!payload || typeof payload !== "object") return []; + const obj = payload as Record; + for (const key of ["items", "bounties", "data", "results"]) { + if (Array.isArray(obj[key])) return obj[key] as AlgoraApiBounty[]; + } + return []; +} + +function normalizeAlgoraApiBounty(item: AlgoraApiBounty): Opportunity | null { + const task = item.task && typeof item.task === "object" ? (item.task as Record) : {}; + const url = String(item.url || item.html_url || item.link || task.url || ""); + if (!url || !/^https?:\/\//.test(url)) return null; + const title = String(item.title || task.title || item.task || item.reward_formatted || "Algora bounty").slice(0, 200); + const reward = extractAlgoraReward(item.reward ?? item.amount ?? item.amount_usd ?? item.reward_usd, `${title} ${item.reward_formatted ?? ""}`); + const techStack = inferTechStack(`${title} ${task.repo_name ?? ""} ${item.org ?? ""} ${item.organization ?? ""}`); + return { + source: "algora", + title, + url, + reward_usd: reward, + tech_stack: techStack, + raw: { + source_shape: "api", + status: String(item.status || ""), + org: String(item.org || item.organization || ""), + reward_formatted: item.reward_formatted ?? null, + task: { + repo_name: task.repo_name ?? null, + number: task.number ?? null, + url: task.url ?? null, + }, + }, + }; +} + +function htmlToText(html: string): string { + return html + .replace(/]*>[\s\S]*?<\/script>/gi, " ") + .replace(/]*>[\s\S]*?<\/style>/gi, " ") + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/'|'/g, "'") + .replace(/"/g, '"') + .replace(/\s+/g, " ") + .trim(); +} + +function parseAlgoraPageBounties(org: string, html: string): Opportunity[] { + const text = htmlToText(html); + const out: Opportunity[] = []; + const seen = new Set(); + const bountyPattern = + /\$\s?([0-9][0-9,]*(?:\.[0-9]+)?)\s+([A-Za-z0-9_.-]+)#([0-9]+)\s+(.+?)(?=\s+\d+\s+(?:days?|weeks?|months?|years?)\s+ago|\s+Image:|\s+\$\s?[0-9]|$)/g; + let match: RegExpExecArray | null; + while ((match = bountyPattern.exec(text)) !== null) { + const reward = Number(match[1].replace(/,/g, "")); + const repo = match[2]; + const issueNumber = match[3]; + const title = match[4].replace(/\s+/g, " ").trim().slice(0, 200); + if (!Number.isFinite(reward) || reward <= 0 || !title) continue; + const url = `https://algora.io/${org}/bounties?status=open#${repo}-${issueNumber}`; + if (seen.has(url)) continue; + seen.add(url); out.push({ source: "algora", - title: title || `Algora bounty`, + title: `${repo}#${issueNumber} ${title}`, url, reward_usd: reward, - raw: { status: String(it.status || ""), org: String(it.org || it.organization || "") }, + tech_stack: inferTechStack(`${org} ${repo} ${title}`), + raw: { + source_shape: "public_org_page", + org, + repo, + issue_number: Number(issueNumber), + source_page: `https://algora.io/${org}/bounties?status=open`, + }, }); } return out; } +// --- Source 3: Algora public bounties (best-effort, no key) --- +// Prefer the documented public API from the bounty task. If Algora serves the +// interactive HTML shell instead of JSON, fall back to public org bounty pages. +async function fetchAlgora(): Promise { + const out: Opportunity[] = []; + const seen = new Set(); + const add = (opps: Opportunity[]) => { + for (const opp of opps) { + if (seen.has(opp.url)) continue; + seen.add(opp.url); + out.push(opp); + } + }; + + const r = await fetchT(ALGORA_API_URL, { + headers: { Accept: "application/json" }, + }); + if (r.ok) { + const contentType = r.headers.get("content-type") || ""; + if (contentType.includes("application/json")) { + const j = await r.json().catch(() => null); + add(parseAlgoraApiItems(j).map(normalizeAlgoraApiBounty).filter((opp): opp is Opportunity => Boolean(opp))); + } else { + const html = await r.text().catch(() => ""); + add(parseAlgoraPageBounties("api", html)); + } + } + + if (out.length >= 10) return out.slice(0, 50); + + for (const org of ALGORA_FALLBACK_ORGS) { + const pageUrl = `https://algora.io/${org}/bounties?status=open`; + const page = await fetchT(pageUrl, { headers: { Accept: "text/html" } }).catch(() => null); + if (!page?.ok) continue; + const html = await page.text().catch(() => ""); + add(parseAlgoraPageBounties(org, html)); + if (out.length >= 50) break; + } + + return out.slice(0, 50); +} + // Queue one real opportunity into runtime_jobs, idempotent on task_id. async function queueOpportunity( sb: ReturnType, @@ -271,6 +420,7 @@ async function queueOpportunity( url: opp.url, title: opp.title, reward_usd: opp.reward_usd, + tech_stack: opp.tech_stack, raw: opp.raw, }, }); From 6f35015cb4d62376e0821c50fb156a72557fd439 Mon Sep 17 00:00:00 2001 From: peterxing <9293096+peterxing@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:16:28 +1000 Subject: [PATCH 2/3] Add Algora fallback proof sample --- docs/algora-opportunity-scout-proof.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/algora-opportunity-scout-proof.md b/docs/algora-opportunity-scout-proof.md index 42bafaf493b..c072d6ac6a6 100644 --- a/docs/algora-opportunity-scout-proof.md +++ b/docs/algora-opportunity-scout-proof.md @@ -40,4 +40,20 @@ https://algora.io/arakoodev/bounties?status=open $50 EdgeChains#290 BOUNTY: integrate AWS Comprehend as a utility to redact data ``` +Second proof check on 2026-07-12 UTC: the public `SCIBASE.AI` Algora page alone exposed 11 open bounties, enough for the "at least 10 real Algora bounties" acceptance path if the API keeps serving HTML. The first 10 parser-visible rows were: + +```text +https://algora.io/SCIBASE.AI/bounties?status=open +$400 SCIBASE.AI#13 AI-Assisted Research Tools (MVP Level) +$500 SCIBASE.AI#20 Revenue Infrastructure +$175 SCIBASE.AI#19 Enterprise Tooling +$1,000 SCIBASE.AI#18 Scientific Bounty System +$475 SCIBASE.AI#17 Scientific Knowledge Graph Integration +$1,325 SCIBASE.AI#16 AI-Powered Research Assistant Suite +$525 SCIBASE.AI#15 Community & User Reputation System +$375 SCIBASE.AI#14 Scientific/Engineering Data & Code Hosting +$700 SCIBASE.AI#12 Real-time collaborative research editor & interface +$500 SCIBASE.AI#11 User & Project Management +``` + Rewards are inserted as extracted numeric USD values, or `null` when no value can be parsed. From d4b8fb8c29155d916efe78ee719fae7e753b374b Mon Sep 17 00:00:00 2001 From: peterxing <9293096+peterxing@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:13:58 +1000 Subject: [PATCH 3/3] Add Algora scout freshness metadata --- docs/algora-opportunity-scout-proof.md | 2 ++ supabase/functions/runtime-opportunity-scout/index.ts | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/docs/algora-opportunity-scout-proof.md b/docs/algora-opportunity-scout-proof.md index c072d6ac6a6..8e46e82268a 100644 --- a/docs/algora-opportunity-scout-proof.md +++ b/docs/algora-opportunity-scout-proof.md @@ -57,3 +57,5 @@ $500 SCIBASE.AI#11 User & Project Management ``` Rewards are inserted as extracted numeric USD values, or `null` when no value can be parsed. + +Freshness note added on 2026-07-13 UTC: Algora opportunities now carry `observed_at` in the queued payload and raw source metadata. This keeps the existing URL-based idempotency key while making stale or inconsistent source-page repeats auditable in `runtime_jobs`. diff --git a/supabase/functions/runtime-opportunity-scout/index.ts b/supabase/functions/runtime-opportunity-scout/index.ts index d515067f25c..c77b98711f1 100644 --- a/supabase/functions/runtime-opportunity-scout/index.ts +++ b/supabase/functions/runtime-opportunity-scout/index.ts @@ -26,6 +26,7 @@ type Opportunity = { url: string; reward_usd: number | null; tech_stack: string[]; + observed_at?: string; raw: Record; }; @@ -278,14 +279,17 @@ function normalizeAlgoraApiBounty(item: AlgoraApiBounty): Opportunity | null { const title = String(item.title || task.title || item.task || item.reward_formatted || "Algora bounty").slice(0, 200); const reward = extractAlgoraReward(item.reward ?? item.amount ?? item.amount_usd ?? item.reward_usd, `${title} ${item.reward_formatted ?? ""}`); const techStack = inferTechStack(`${title} ${task.repo_name ?? ""} ${item.org ?? ""} ${item.organization ?? ""}`); + const observedAt = new Date().toISOString(); return { source: "algora", title, url, reward_usd: reward, tech_stack: techStack, + observed_at: observedAt, raw: { source_shape: "api", + observed_at: observedAt, status: String(item.status || ""), org: String(item.org || item.organization || ""), reward_formatted: item.reward_formatted ?? null, @@ -315,6 +319,7 @@ function parseAlgoraPageBounties(org: string, html: string): Opportunity[] { const text = htmlToText(html); const out: Opportunity[] = []; const seen = new Set(); + const observedAt = new Date().toISOString(); const bountyPattern = /\$\s?([0-9][0-9,]*(?:\.[0-9]+)?)\s+([A-Za-z0-9_.-]+)#([0-9]+)\s+(.+?)(?=\s+\d+\s+(?:days?|weeks?|months?|years?)\s+ago|\s+Image:|\s+\$\s?[0-9]|$)/g; let match: RegExpExecArray | null; @@ -333,8 +338,10 @@ function parseAlgoraPageBounties(org: string, html: string): Opportunity[] { url, reward_usd: reward, tech_stack: inferTechStack(`${org} ${repo} ${title}`), + observed_at: observedAt, raw: { source_shape: "public_org_page", + observed_at: observedAt, org, repo, issue_number: Number(issueNumber), @@ -404,6 +411,7 @@ async function queueOpportunity( // Concrete reward => solve it; unknown reward => qualify it first via research. const taskKind = opp.reward_usd != null ? "bounty_solving" : "research"; const priority = rewardPriority(opp.reward_usd); + const observedAt = opp.observed_at ?? new Date().toISOString(); const { error } = await sb.from("runtime_jobs").insert({ task_id: taskId, @@ -421,6 +429,7 @@ async function queueOpportunity( title: opp.title, reward_usd: opp.reward_usd, tech_stack: opp.tech_stack, + observed_at: observedAt, raw: opp.raw, }, });