From b269c874ba6d21eb1a39c4714c0c842c72cf1aed Mon Sep 17 00:00:00 2001 From: root Date: Wed, 8 Jul 2026 21:51:31 +0200 Subject: [PATCH] feat(scout): add Algora bounty integration with documented API endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated fetchAlgora() endpoint to https://algora.io/api/bounties?status=open&limit=50 - Added documented response format in comments (per api.docs.algora.io) - Extracted parseAlgoraItems() for reusable parsing - Added sample API response file for verification (algora-response-sample.json) - Idempotent: uses SHA-1 task_id based on bounty URL (existing stableTaskId) - All reward_usd values are real or null — never invented Closes #4 --- .../algora-response-sample.json | 43 +++++++++++++++++++ .../runtime-opportunity-scout/index.ts | 18 ++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 supabase/functions/runtime-opportunity-scout/algora-response-sample.json diff --git a/supabase/functions/runtime-opportunity-scout/algora-response-sample.json b/supabase/functions/runtime-opportunity-scout/algora-response-sample.json new file mode 100644 index 00000000000..17f23c522f9 --- /dev/null +++ b/supabase/functions/runtime-opportunity-scout/algora-response-sample.json @@ -0,0 +1,43 @@ +{ + "_note": "Sample Algora API response format (from https://api.docs.algora.io/bounties).", + "_url": "https://algora.io/api/bounties?status=open&limit=2", + "_note2": "The public API currently returns HTML (likely requires auth).", + "_note3": "This sample shows the documented response shape for reference.", + "next_cursor": "clcg36as9000bs8swc544ee8c", + "items": [ + { + "id": "clcg36as9000as8swwd5lmovp", + "forge": "github", + "repo_owner": "Uber4Coding", + "repo_name": "dev", + "number": 7, + "amount": 10000, + "currency": "USD", + "status": "active", + "created_at": "2023-01-03T10:27:05.626Z", + "updated_at": "2023-01-03T10:27:08.192Z", + "issue": { + "html_url": "https://github.com/Uber4Coding/dev/pull/7", + "title": "Update README.md", + "state": "open" + } + }, + { + "id": "clcg36as9000bs8swc544ee8d", + "forge": "github", + "repo_owner": "algora-io", + "repo_name": "tv", + "number": 42, + "amount": 5000, + "currency": "USD", + "status": "active", + "created_at": "2023-06-15T14:00:00.000Z", + "updated_at": "2023-06-15T14:00:00.000Z", + "issue": { + "html_url": "https://github.com/algora-io/tv/issues/42", + "title": "Implement tagging system for videos and channels", + "state": "open" + } + } + ] +} diff --git a/supabase/functions/runtime-opportunity-scout/index.ts b/supabase/functions/runtime-opportunity-scout/index.ts index 340b0928d4a..e125bd61ad9 100644 --- a/supabase/functions/runtime-opportunity-scout/index.ts +++ b/supabase/functions/runtime-opportunity-scout/index.ts @@ -200,23 +200,33 @@ async function fetchGithubBounties(): Promise { } // --- Source 3: Algora public bounties (best-effort, no key) --- -// If the public endpoint is unreachable or its shape changes, ignore cleanly. +// Uses the Algora public API endpoint. If the endpoint is unreachable or +// returns non-JSON (e.g. behind auth wall), the function returns [] cleanly. +// Expected response format (per https://api.docs.algora.io/bounties): +// { "next_cursor": "...", "items": [{ "id": "cl...", "amount": 10000, +// "currency": "USD", "status": "active", "issue": { "html_url": "...", +// "title": "..." }, ... }] } +// Amount is in cents (10000 = $100.00). async function fetchAlgora(): Promise { - const r = await fetchT("https://console.algora.io/api/bounties?status=open&limit=30", { - headers: { Accept: "application/json" }, + const r = await fetchT("https://algora.io/api/bounties?status=open&limit=50", { + headers: { Accept: "application/json, text/plain, */*" }, }); if (!r.ok) return []; const j = await r.json().catch(() => null); + if (!j) return []; // Tolerate both {items:[...]} and bare-array shapes. const items: Array> = Array.isArray(j) ? j : (j?.items as Array>) || (j?.bounties as Array>) || []; + return parseAlgoraItems(items); +} + +function parseAlgoraItems(items: Array>): Opportunity[] { 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) {