Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
81 KiB
JSON
1 line
81 KiB
JSON
{"repo": "bjtheartist/chistartuphub", "files": {"tools/investor-harvester/src/backfill-locations.js": {"f5ca936971b96054f07d4b5ae007ed67e3399c3f": {"t": 1785746703.369635, "n": 10855, "h": "940bd0e0a2893b52", "c": "// backfill-locations.js — Extract HQ location from search_profile/description using DeepSeek\n// Then update investors via Supabase REST API\n\nconst fs = require('fs')\nconst fetch = require('node-fetch')\n\n// ── Config ──────────────────────────────────────────────────────────────────\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co'\nconst SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E'\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4'\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions'\nconst MODEL = 'deepseek-chat'\n\nconst CONCURRENCY = 5\nconst BATCH_DELAY_MS = 200\nconst RESULTS_PATH = '/tmp/location-backfill-results.json'\n\n// Whether to use RPC function instead of direct PATCH\n// PATCH is blocked by RLS, so RPC is the default\nlet USE_RPC = true\n\n// ── Region mapping ──────────────────────────────────────────────────────────\nconst REGION_MAP = {\n 'midwest': ['Illinois', 'Wisconsin', 'Indiana', 'Michigan', 'Ohio', 'Minnesota', 'Iowa', 'Missouri', 'Nebraska', 'Kansas', 'North Dakota', 'South Dakota'],\n 'east-coast': ['New York', 'New Jersey', 'Connecticut', 'Massachusetts', 'Pennsylvania', 'Maryland', 'DC', 'Virginia', 'Delaware', 'Rhode Island', 'New Hampshire', 'Vermont', 'Maine'],\n 'west-coast': ['California', 'Washington', 'Oregon', 'Hawaii'],\n 'south': ['Texas', 'Florida', 'Georgia', 'North Carolina', 'South Carolina', 'Tennessee', 'Alabama', 'Louisiana', 'Mississippi', 'Arkansas', 'Kentucky', 'West Virginia', 'Oklahoma'],\n 'mountain-west': ['Colorado', 'Utah', 'Arizona', 'Nevada', 'New Mexico', 'Idaho', 'Montana', 'Wyoming', 'Alaska'],\n}\n\nconst VALID_REGIONS = Object.keys(REGION_MAP)\n\nconst PROMPT_TEMPLATE = `Given this investor's profile, determine their headquarters location.\nReturn ONLY a JSON object: {\"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- state must be the full US state name (e.g., \"California\", \"Illinois\", \"New York\")\n- region must be one of: midwest, west-coast, east-coast, south, mountain-west\n- If you cannot determine location, return {\"city\": null, \"state\": null, \"region\": null}\n\nRegion mapping:\n- midwest: Illinois, Wisconsin, Indiana, Michigan, Ohio, Minnesota, Iowa, Missouri, Nebraska, Kansas, North Dakota, South Dakota\n- east-coast: New York, New Jersey, Connecticut, Massachusetts, Pennsylvania, Maryland, DC, Virginia, Delaware, Rhode Island, New Hampshire, Vermont, Maine\n- west-coast: California, Washington, Oregon, Hawaii\n- south: Texas, Florida, Georgia, North Carolina, South Carolina, Tennessee, Alabama, Louisiana, Mississippi, Arkansas, Kentucky, West Virginia, Oklahoma\n- mountain-west: Colorado, Utah, Arizona, Nevada, New Mexico, Idaho, Montana, Wyoming, Alaska\n\nInvestor: {canonical_name}\nWebsite: {website}\nProfile: {search_profile}`\n\n// ── Supabase helpers ────────────────────────────────────────────────────────\n\nconst supabaseHeaders = {\n 'apikey': SUPABASE_KEY,\n 'Authorization': `Bearer ${SUPABASE_KEY}`,\n 'Content-Type': 'application/json',\n}\n\nasync function fetchInvestorsMissingLocation() {\n const allInvestors = []\n let offset = 0\n const pageSize = 1000\n\n while (true) {\n const url = `${SUPABASE_URL}/rest/v1/investors?hq_state=is.null&hq_country=eq.USA&select=id,canonical_name,website,description,search_profile&offset=${offset}&limit=${pageSize}`\n const res = await fetch(url, { headers: supabaseHeaders })\n\n if (!res.ok) {\n throw new Error(`Supabase fetch failed: ${res.status} ${await res.text()}`)\n }\n\n const batch = await res.json()\n allInvestors.push(...batch)\n\n if (batch.length < pageSize) break\n offset += pageSize\n }\n\n return allInvestors\n}\n\nasync function updateInvestorPATCH(id, city, state, region) {\n const body = {}\n if (city !== null) body.hq_city = city\n if (state !== null) body.hq_state = state\n if (region !== null) body.hq_region = region\n\n if (Object.keys(body).length === 0) return { ok: true, skipped: true }\n\n const url = `${SUPABASE_URL}/rest/v1/investors?id=eq.${id}`\n const res = await fetch(url, {\n method: 'PATCH',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify(body),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestorRPC(id, city, state, region) {\n const url = `${SUPABASE_URL}/rest/v1/rpc/update_investor_location`\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify({\n investor_id: id,\n new_city: city,\n new_state: state,\n new_region: region,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestor(id, city, state, region) {\n if (USE_RPC) {\n return updateInvestorRPC(id, city, state, region)\n }\n return updateInvestorPATCH(id, city, state, region)\n}\n\n// ── DeepSeek AI ─────────────────────────────────────────────────────────────\n\nasync function extractLocation(investor) {\n const prompt = PROMPT_TEMPLATE\n .replace('{canonical_name}', investor.canonical_name || 'Unknown')\n .replace('{website}', investor.website || 'N/A')\n .replace('{search_profile}', (investor.search_profile || investor.description || 'N/A').slice(0, 2000))\n\n try {\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: MODEL,\n messages: [{ role: 'user', content: prompt }],\n max_tokens: 150,\n temperature: 0.2,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n throw new Error(`DeepSeek API error ${res.status}: ${errText}`)\n }\n\n const data = await res.json()\n const raw = data.choices[0].message.content.trim()\n const jsonStr = raw.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/i, '')\n const parsed = JSON.parse(jsonStr)\n\n // Validate\n const city = typeof parsed.city === 'string' && parsed.city.length > 0 ? parsed.city : null\n const state = typeof parsed.state === 'string' && parsed.state.length > 0 ? parsed.state : null\n let region = typeof parsed.region === 'string' ? parsed.region.toLowerCase() : null\n if (region && !VALID_REGIONS.includes(region)) region = null\n\n // If we have a state but no region, derive it\n if (state && !region) {\n for (const [r, states] of Object.entries(REGION_MAP)) {\n if (states.some(s => s.toLowerCase() === state.toLowerCase())) {\n region = r\n break\n }\n }\n }\n\n return { success: true, city, state, region }\n } catch (error) {\n return { success: false, error: error.message, city: null, state: null, region: null }\n }\n}\n\n// ── Concurrency helper ──────────────────────────────────────────────────────\n\nasync function processInBatches(items, batchSize, delayMs, fn) {\n const results = []\n for (let i = 0; i < items.length; i += batchSize) {\n const batch = items.slice(i, i + batchSize)\n const batchResults = await Promise.all(batch.map(fn))\n results.push(...batchResults)\n if (i + batchSize < items.length) {\n await new Promise(r => setTimeout(r, delayMs))\n }\n }\n return results\n}\n\n// ── Main ────────────────────────────────────────────────────────────────────\n\nasync function main() {\n console.log('=== Investor Location Backfill ===\\n')\n\n // Step 1: Fetch investors missing location\n console.log('Fetching investors with missing hq_state (USA only)...')\n const investors = await fetchInvestorsMissingLocation()\n console.log(`Found ${investors.length} investors to process.\\n`)\n\n if (investors.length === 0) {\n console.log('Nothing to do. Exiting.')\n return\n }\n\n // Step 2: Extract locations via DeepSeek\n console.log(`Extracting locations via DeepSeek (concurrency=${CONCURRENCY})...\\n`)\n\n const results = []\n let processed = 0\n let locationsFound = 0\n let locationsFailed = 0\n let updateErrors = 0\n\n const allResults = await processInBatches(investors, CONCURRENCY, BATCH_DELAY_MS, async (inv) => {\n const loc = await extractLocation(inv)\n processed++\n\n if (processed % 50 === 0 || processed === investors.length) {\n console.log(` Progress: ${processed}/${investors.length} processed (${locationsFound} locations found so far)`)\n }\n\n const result = {\n id: inv.id,\n canonical_name: inv.canonical_name,\n extracted: loc,\n updated: false,\n }\n\n if (loc.success && (loc.city || loc.state || loc.region)) {\n locationsFound++\n\n // Step 3: Update in Supabase\n const updateResult = await updateInvestor(inv.id, loc.city, loc.state, loc.region)\n\n if (!updateResult.ok) {\n // If PATCH fails due to RLS, switch to RPC\n if (!USE_RPC && (updateResult.status === 403 || (updateResult.error && updateResult.error.includes('policy')))) {\n console.log(`\\n PATCH blocked by RLS (status ${updateResult.status}). Switching to RPC mode...\\n`)\n USE_RPC = true\n\n // Retry this one with RPC\n const retryResult = await updateInvestorRPC(inv.id, loc.city, loc.state, loc.region)\n if (retryResult.ok) {\n result.updated = true\n } else {\n console.log(` RPC also failed for ${inv.canonical_name}: ${retryResult.error}`)\n updateErrors++\n result.updateError = retryResult.error\n }\n } else {\n updateErrors++\n result.updateError = updateResult.error\n }\n } else {\n result.updated = true\n }\n } else {\n locationsFailed++\n }\n\n return result\n })\n\n // Step 4: Save results\n const output = {\n timestamp: new Date().toISOString(),\n summary: {\n total: investors.length,\n locationsFound,\n stillNull: locationsFailed,\n updateErrors,\n useRPC: USE_RPC,\n },\n results: allResults,\n }\n\n fs.writeFileSync(RESULTS_PATH, JSON.stringify(output, null, 2))\n console.log(`\\nResults saved to ${RESULTS_PATH}`)\n\n // Step 5: Summary\n console.log('\\n=== SUMMARY ===')\n console.log(`Total processed: ${investors.length}`)\n console.log(`Locations found: ${locationsFound}`)\n console.log(`Still null: ${locationsFailed}`)\n console.log(`Update errors: ${updateErrors}`)\n console.log(`Update method: ${USE_RPC ? 'RPC' : 'PATCH'}`)\n console.log('================\\n')\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err)\n process.exit(1)\n})\n"}, "d8888e64b5c232e6e53e41dac148e23d3b6f2c3f": {"t": 1785767095.9772007, "n": 10855, "h": "940bd0e0a2893b52", "c": "// backfill-locations.js — Extract HQ location from search_profile/description using DeepSeek\n// Then update investors via Supabase REST API\n\nconst fs = require('fs')\nconst fetch = require('node-fetch')\n\n// ── Config ──────────────────────────────────────────────────────────────────\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co'\nconst SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E'\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4'\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions'\nconst MODEL = 'deepseek-chat'\n\nconst CONCURRENCY = 5\nconst BATCH_DELAY_MS = 200\nconst RESULTS_PATH = '/tmp/location-backfill-results.json'\n\n// Whether to use RPC function instead of direct PATCH\n// PATCH is blocked by RLS, so RPC is the default\nlet USE_RPC = true\n\n// ── Region mapping ──────────────────────────────────────────────────────────\nconst REGION_MAP = {\n 'midwest': ['Illinois', 'Wisconsin', 'Indiana', 'Michigan', 'Ohio', 'Minnesota', 'Iowa', 'Missouri', 'Nebraska', 'Kansas', 'North Dakota', 'South Dakota'],\n 'east-coast': ['New York', 'New Jersey', 'Connecticut', 'Massachusetts', 'Pennsylvania', 'Maryland', 'DC', 'Virginia', 'Delaware', 'Rhode Island', 'New Hampshire', 'Vermont', 'Maine'],\n 'west-coast': ['California', 'Washington', 'Oregon', 'Hawaii'],\n 'south': ['Texas', 'Florida', 'Georgia', 'North Carolina', 'South Carolina', 'Tennessee', 'Alabama', 'Louisiana', 'Mississippi', 'Arkansas', 'Kentucky', 'West Virginia', 'Oklahoma'],\n 'mountain-west': ['Colorado', 'Utah', 'Arizona', 'Nevada', 'New Mexico', 'Idaho', 'Montana', 'Wyoming', 'Alaska'],\n}\n\nconst VALID_REGIONS = Object.keys(REGION_MAP)\n\nconst PROMPT_TEMPLATE = `Given this investor's profile, determine their headquarters location.\nReturn ONLY a JSON object: {\"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- state must be the full US state name (e.g., \"California\", \"Illinois\", \"New York\")\n- region must be one of: midwest, west-coast, east-coast, south, mountain-west\n- If you cannot determine location, return {\"city\": null, \"state\": null, \"region\": null}\n\nRegion mapping:\n- midwest: Illinois, Wisconsin, Indiana, Michigan, Ohio, Minnesota, Iowa, Missouri, Nebraska, Kansas, North Dakota, South Dakota\n- east-coast: New York, New Jersey, Connecticut, Massachusetts, Pennsylvania, Maryland, DC, Virginia, Delaware, Rhode Island, New Hampshire, Vermont, Maine\n- west-coast: California, Washington, Oregon, Hawaii\n- south: Texas, Florida, Georgia, North Carolina, South Carolina, Tennessee, Alabama, Louisiana, Mississippi, Arkansas, Kentucky, West Virginia, Oklahoma\n- mountain-west: Colorado, Utah, Arizona, Nevada, New Mexico, Idaho, Montana, Wyoming, Alaska\n\nInvestor: {canonical_name}\nWebsite: {website}\nProfile: {search_profile}`\n\n// ── Supabase helpers ────────────────────────────────────────────────────────\n\nconst supabaseHeaders = {\n 'apikey': SUPABASE_KEY,\n 'Authorization': `Bearer ${SUPABASE_KEY}`,\n 'Content-Type': 'application/json',\n}\n\nasync function fetchInvestorsMissingLocation() {\n const allInvestors = []\n let offset = 0\n const pageSize = 1000\n\n while (true) {\n const url = `${SUPABASE_URL}/rest/v1/investors?hq_state=is.null&hq_country=eq.USA&select=id,canonical_name,website,description,search_profile&offset=${offset}&limit=${pageSize}`\n const res = await fetch(url, { headers: supabaseHeaders })\n\n if (!res.ok) {\n throw new Error(`Supabase fetch failed: ${res.status} ${await res.text()}`)\n }\n\n const batch = await res.json()\n allInvestors.push(...batch)\n\n if (batch.length < pageSize) break\n offset += pageSize\n }\n\n return allInvestors\n}\n\nasync function updateInvestorPATCH(id, city, state, region) {\n const body = {}\n if (city !== null) body.hq_city = city\n if (state !== null) body.hq_state = state\n if (region !== null) body.hq_region = region\n\n if (Object.keys(body).length === 0) return { ok: true, skipped: true }\n\n const url = `${SUPABASE_URL}/rest/v1/investors?id=eq.${id}`\n const res = await fetch(url, {\n method: 'PATCH',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify(body),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestorRPC(id, city, state, region) {\n const url = `${SUPABASE_URL}/rest/v1/rpc/update_investor_location`\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify({\n investor_id: id,\n new_city: city,\n new_state: state,\n new_region: region,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestor(id, city, state, region) {\n if (USE_RPC) {\n return updateInvestorRPC(id, city, state, region)\n }\n return updateInvestorPATCH(id, city, state, region)\n}\n\n// ── DeepSeek AI ─────────────────────────────────────────────────────────────\n\nasync function extractLocation(investor) {\n const prompt = PROMPT_TEMPLATE\n .replace('{canonical_name}', investor.canonical_name || 'Unknown')\n .replace('{website}', investor.website || 'N/A')\n .replace('{search_profile}', (investor.search_profile || investor.description || 'N/A').slice(0, 2000))\n\n try {\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: MODEL,\n messages: [{ role: 'user', content: prompt }],\n max_tokens: 150,\n temperature: 0.2,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n throw new Error(`DeepSeek API error ${res.status}: ${errText}`)\n }\n\n const data = await res.json()\n const raw = data.choices[0].message.content.trim()\n const jsonStr = raw.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/i, '')\n const parsed = JSON.parse(jsonStr)\n\n // Validate\n const city = typeof parsed.city === 'string' && parsed.city.length > 0 ? parsed.city : null\n const state = typeof parsed.state === 'string' && parsed.state.length > 0 ? parsed.state : null\n let region = typeof parsed.region === 'string' ? parsed.region.toLowerCase() : null\n if (region && !VALID_REGIONS.includes(region)) region = null\n\n // If we have a state but no region, derive it\n if (state && !region) {\n for (const [r, states] of Object.entries(REGION_MAP)) {\n if (states.some(s => s.toLowerCase() === state.toLowerCase())) {\n region = r\n break\n }\n }\n }\n\n return { success: true, city, state, region }\n } catch (error) {\n return { success: false, error: error.message, city: null, state: null, region: null }\n }\n}\n\n// ── Concurrency helper ──────────────────────────────────────────────────────\n\nasync function processInBatches(items, batchSize, delayMs, fn) {\n const results = []\n for (let i = 0; i < items.length; i += batchSize) {\n const batch = items.slice(i, i + batchSize)\n const batchResults = await Promise.all(batch.map(fn))\n results.push(...batchResults)\n if (i + batchSize < items.length) {\n await new Promise(r => setTimeout(r, delayMs))\n }\n }\n return results\n}\n\n// ── Main ────────────────────────────────────────────────────────────────────\n\nasync function main() {\n console.log('=== Investor Location Backfill ===\\n')\n\n // Step 1: Fetch investors missing location\n console.log('Fetching investors with missing hq_state (USA only)...')\n const investors = await fetchInvestorsMissingLocation()\n console.log(`Found ${investors.length} investors to process.\\n`)\n\n if (investors.length === 0) {\n console.log('Nothing to do. Exiting.')\n return\n }\n\n // Step 2: Extract locations via DeepSeek\n console.log(`Extracting locations via DeepSeek (concurrency=${CONCURRENCY})...\\n`)\n\n const results = []\n let processed = 0\n let locationsFound = 0\n let locationsFailed = 0\n let updateErrors = 0\n\n const allResults = await processInBatches(investors, CONCURRENCY, BATCH_DELAY_MS, async (inv) => {\n const loc = await extractLocation(inv)\n processed++\n\n if (processed % 50 === 0 || processed === investors.length) {\n console.log(` Progress: ${processed}/${investors.length} processed (${locationsFound} locations found so far)`)\n }\n\n const result = {\n id: inv.id,\n canonical_name: inv.canonical_name,\n extracted: loc,\n updated: false,\n }\n\n if (loc.success && (loc.city || loc.state || loc.region)) {\n locationsFound++\n\n // Step 3: Update in Supabase\n const updateResult = await updateInvestor(inv.id, loc.city, loc.state, loc.region)\n\n if (!updateResult.ok) {\n // If PATCH fails due to RLS, switch to RPC\n if (!USE_RPC && (updateResult.status === 403 || (updateResult.error && updateResult.error.includes('policy')))) {\n console.log(`\\n PATCH blocked by RLS (status ${updateResult.status}). Switching to RPC mode...\\n`)\n USE_RPC = true\n\n // Retry this one with RPC\n const retryResult = await updateInvestorRPC(inv.id, loc.city, loc.state, loc.region)\n if (retryResult.ok) {\n result.updated = true\n } else {\n console.log(` RPC also failed for ${inv.canonical_name}: ${retryResult.error}`)\n updateErrors++\n result.updateError = retryResult.error\n }\n } else {\n updateErrors++\n result.updateError = updateResult.error\n }\n } else {\n result.updated = true\n }\n } else {\n locationsFailed++\n }\n\n return result\n })\n\n // Step 4: Save results\n const output = {\n timestamp: new Date().toISOString(),\n summary: {\n total: investors.length,\n locationsFound,\n stillNull: locationsFailed,\n updateErrors,\n useRPC: USE_RPC,\n },\n results: allResults,\n }\n\n fs.writeFileSync(RESULTS_PATH, JSON.stringify(output, null, 2))\n console.log(`\\nResults saved to ${RESULTS_PATH}`)\n\n // Step 5: Summary\n console.log('\\n=== SUMMARY ===')\n console.log(`Total processed: ${investors.length}`)\n console.log(`Locations found: ${locationsFound}`)\n console.log(`Still null: ${locationsFailed}`)\n console.log(`Update errors: ${updateErrors}`)\n console.log(`Update method: ${USE_RPC ? 'RPC' : 'PATCH'}`)\n console.log('================\\n')\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err)\n process.exit(1)\n})\n"}, "06098332267a6a2bbdee585f61c04c6b89d71023": {"t": 1786067874.5124269, "n": 10855, "h": "940bd0e0a2893b52", "c": "// backfill-locations.js — Extract HQ location from search_profile/description using DeepSeek\n// Then update investors via Supabase REST API\n\nconst fs = require('fs')\nconst fetch = require('node-fetch')\n\n// ── Config ──────────────────────────────────────────────────────────────────\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co'\nconst SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E'\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4'\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions'\nconst MODEL = 'deepseek-chat'\n\nconst CONCURRENCY = 5\nconst BATCH_DELAY_MS = 200\nconst RESULTS_PATH = '/tmp/location-backfill-results.json'\n\n// Whether to use RPC function instead of direct PATCH\n// PATCH is blocked by RLS, so RPC is the default\nlet USE_RPC = true\n\n// ── Region mapping ──────────────────────────────────────────────────────────\nconst REGION_MAP = {\n 'midwest': ['Illinois', 'Wisconsin', 'Indiana', 'Michigan', 'Ohio', 'Minnesota', 'Iowa', 'Missouri', 'Nebraska', 'Kansas', 'North Dakota', 'South Dakota'],\n 'east-coast': ['New York', 'New Jersey', 'Connecticut', 'Massachusetts', 'Pennsylvania', 'Maryland', 'DC', 'Virginia', 'Delaware', 'Rhode Island', 'New Hampshire', 'Vermont', 'Maine'],\n 'west-coast': ['California', 'Washington', 'Oregon', 'Hawaii'],\n 'south': ['Texas', 'Florida', 'Georgia', 'North Carolina', 'South Carolina', 'Tennessee', 'Alabama', 'Louisiana', 'Mississippi', 'Arkansas', 'Kentucky', 'West Virginia', 'Oklahoma'],\n 'mountain-west': ['Colorado', 'Utah', 'Arizona', 'Nevada', 'New Mexico', 'Idaho', 'Montana', 'Wyoming', 'Alaska'],\n}\n\nconst VALID_REGIONS = Object.keys(REGION_MAP)\n\nconst PROMPT_TEMPLATE = `Given this investor's profile, determine their headquarters location.\nReturn ONLY a JSON object: {\"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- state must be the full US state name (e.g., \"California\", \"Illinois\", \"New York\")\n- region must be one of: midwest, west-coast, east-coast, south, mountain-west\n- If you cannot determine location, return {\"city\": null, \"state\": null, \"region\": null}\n\nRegion mapping:\n- midwest: Illinois, Wisconsin, Indiana, Michigan, Ohio, Minnesota, Iowa, Missouri, Nebraska, Kansas, North Dakota, South Dakota\n- east-coast: New York, New Jersey, Connecticut, Massachusetts, Pennsylvania, Maryland, DC, Virginia, Delaware, Rhode Island, New Hampshire, Vermont, Maine\n- west-coast: California, Washington, Oregon, Hawaii\n- south: Texas, Florida, Georgia, North Carolina, South Carolina, Tennessee, Alabama, Louisiana, Mississippi, Arkansas, Kentucky, West Virginia, Oklahoma\n- mountain-west: Colorado, Utah, Arizona, Nevada, New Mexico, Idaho, Montana, Wyoming, Alaska\n\nInvestor: {canonical_name}\nWebsite: {website}\nProfile: {search_profile}`\n\n// ── Supabase helpers ────────────────────────────────────────────────────────\n\nconst supabaseHeaders = {\n 'apikey': SUPABASE_KEY,\n 'Authorization': `Bearer ${SUPABASE_KEY}`,\n 'Content-Type': 'application/json',\n}\n\nasync function fetchInvestorsMissingLocation() {\n const allInvestors = []\n let offset = 0\n const pageSize = 1000\n\n while (true) {\n const url = `${SUPABASE_URL}/rest/v1/investors?hq_state=is.null&hq_country=eq.USA&select=id,canonical_name,website,description,search_profile&offset=${offset}&limit=${pageSize}`\n const res = await fetch(url, { headers: supabaseHeaders })\n\n if (!res.ok) {\n throw new Error(`Supabase fetch failed: ${res.status} ${await res.text()}`)\n }\n\n const batch = await res.json()\n allInvestors.push(...batch)\n\n if (batch.length < pageSize) break\n offset += pageSize\n }\n\n return allInvestors\n}\n\nasync function updateInvestorPATCH(id, city, state, region) {\n const body = {}\n if (city !== null) body.hq_city = city\n if (state !== null) body.hq_state = state\n if (region !== null) body.hq_region = region\n\n if (Object.keys(body).length === 0) return { ok: true, skipped: true }\n\n const url = `${SUPABASE_URL}/rest/v1/investors?id=eq.${id}`\n const res = await fetch(url, {\n method: 'PATCH',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify(body),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestorRPC(id, city, state, region) {\n const url = `${SUPABASE_URL}/rest/v1/rpc/update_investor_location`\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify({\n investor_id: id,\n new_city: city,\n new_state: state,\n new_region: region,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestor(id, city, state, region) {\n if (USE_RPC) {\n return updateInvestorRPC(id, city, state, region)\n }\n return updateInvestorPATCH(id, city, state, region)\n}\n\n// ── DeepSeek AI ─────────────────────────────────────────────────────────────\n\nasync function extractLocation(investor) {\n const prompt = PROMPT_TEMPLATE\n .replace('{canonical_name}', investor.canonical_name || 'Unknown')\n .replace('{website}', investor.website || 'N/A')\n .replace('{search_profile}', (investor.search_profile || investor.description || 'N/A').slice(0, 2000))\n\n try {\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: MODEL,\n messages: [{ role: 'user', content: prompt }],\n max_tokens: 150,\n temperature: 0.2,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n throw new Error(`DeepSeek API error ${res.status}: ${errText}`)\n }\n\n const data = await res.json()\n const raw = data.choices[0].message.content.trim()\n const jsonStr = raw.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/i, '')\n const parsed = JSON.parse(jsonStr)\n\n // Validate\n const city = typeof parsed.city === 'string' && parsed.city.length > 0 ? parsed.city : null\n const state = typeof parsed.state === 'string' && parsed.state.length > 0 ? parsed.state : null\n let region = typeof parsed.region === 'string' ? parsed.region.toLowerCase() : null\n if (region && !VALID_REGIONS.includes(region)) region = null\n\n // If we have a state but no region, derive it\n if (state && !region) {\n for (const [r, states] of Object.entries(REGION_MAP)) {\n if (states.some(s => s.toLowerCase() === state.toLowerCase())) {\n region = r\n break\n }\n }\n }\n\n return { success: true, city, state, region }\n } catch (error) {\n return { success: false, error: error.message, city: null, state: null, region: null }\n }\n}\n\n// ── Concurrency helper ──────────────────────────────────────────────────────\n\nasync function processInBatches(items, batchSize, delayMs, fn) {\n const results = []\n for (let i = 0; i < items.length; i += batchSize) {\n const batch = items.slice(i, i + batchSize)\n const batchResults = await Promise.all(batch.map(fn))\n results.push(...batchResults)\n if (i + batchSize < items.length) {\n await new Promise(r => setTimeout(r, delayMs))\n }\n }\n return results\n}\n\n// ── Main ────────────────────────────────────────────────────────────────────\n\nasync function main() {\n console.log('=== Investor Location Backfill ===\\n')\n\n // Step 1: Fetch investors missing location\n console.log('Fetching investors with missing hq_state (USA only)...')\n const investors = await fetchInvestorsMissingLocation()\n console.log(`Found ${investors.length} investors to process.\\n`)\n\n if (investors.length === 0) {\n console.log('Nothing to do. Exiting.')\n return\n }\n\n // Step 2: Extract locations via DeepSeek\n console.log(`Extracting locations via DeepSeek (concurrency=${CONCURRENCY})...\\n`)\n\n const results = []\n let processed = 0\n let locationsFound = 0\n let locationsFailed = 0\n let updateErrors = 0\n\n const allResults = await processInBatches(investors, CONCURRENCY, BATCH_DELAY_MS, async (inv) => {\n const loc = await extractLocation(inv)\n processed++\n\n if (processed % 50 === 0 || processed === investors.length) {\n console.log(` Progress: ${processed}/${investors.length} processed (${locationsFound} locations found so far)`)\n }\n\n const result = {\n id: inv.id,\n canonical_name: inv.canonical_name,\n extracted: loc,\n updated: false,\n }\n\n if (loc.success && (loc.city || loc.state || loc.region)) {\n locationsFound++\n\n // Step 3: Update in Supabase\n const updateResult = await updateInvestor(inv.id, loc.city, loc.state, loc.region)\n\n if (!updateResult.ok) {\n // If PATCH fails due to RLS, switch to RPC\n if (!USE_RPC && (updateResult.status === 403 || (updateResult.error && updateResult.error.includes('policy')))) {\n console.log(`\\n PATCH blocked by RLS (status ${updateResult.status}). Switching to RPC mode...\\n`)\n USE_RPC = true\n\n // Retry this one with RPC\n const retryResult = await updateInvestorRPC(inv.id, loc.city, loc.state, loc.region)\n if (retryResult.ok) {\n result.updated = true\n } else {\n console.log(` RPC also failed for ${inv.canonical_name}: ${retryResult.error}`)\n updateErrors++\n result.updateError = retryResult.error\n }\n } else {\n updateErrors++\n result.updateError = updateResult.error\n }\n } else {\n result.updated = true\n }\n } else {\n locationsFailed++\n }\n\n return result\n })\n\n // Step 4: Save results\n const output = {\n timestamp: new Date().toISOString(),\n summary: {\n total: investors.length,\n locationsFound,\n stillNull: locationsFailed,\n updateErrors,\n useRPC: USE_RPC,\n },\n results: allResults,\n }\n\n fs.writeFileSync(RESULTS_PATH, JSON.stringify(output, null, 2))\n console.log(`\\nResults saved to ${RESULTS_PATH}`)\n\n // Step 5: Summary\n console.log('\\n=== SUMMARY ===')\n console.log(`Total processed: ${investors.length}`)\n console.log(`Locations found: ${locationsFound}`)\n console.log(`Still null: ${locationsFailed}`)\n console.log(`Update errors: ${updateErrors}`)\n console.log(`Update method: ${USE_RPC ? 'RPC' : 'PATCH'}`)\n console.log('================\\n')\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err)\n process.exit(1)\n})\n"}, "be95a9d7fbfaae474b70eadfca5a956cf6e99169": {"t": 1786068489.0875692, "n": 10855, "h": "940bd0e0a2893b52", "c": "// backfill-locations.js — Extract HQ location from search_profile/description using DeepSeek\n// Then update investors via Supabase REST API\n\nconst fs = require('fs')\nconst fetch = require('node-fetch')\n\n// ── Config ──────────────────────────────────────────────────────────────────\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co'\nconst SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E'\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4'\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions'\nconst MODEL = 'deepseek-chat'\n\nconst CONCURRENCY = 5\nconst BATCH_DELAY_MS = 200\nconst RESULTS_PATH = '/tmp/location-backfill-results.json'\n\n// Whether to use RPC function instead of direct PATCH\n// PATCH is blocked by RLS, so RPC is the default\nlet USE_RPC = true\n\n// ── Region mapping ──────────────────────────────────────────────────────────\nconst REGION_MAP = {\n 'midwest': ['Illinois', 'Wisconsin', 'Indiana', 'Michigan', 'Ohio', 'Minnesota', 'Iowa', 'Missouri', 'Nebraska', 'Kansas', 'North Dakota', 'South Dakota'],\n 'east-coast': ['New York', 'New Jersey', 'Connecticut', 'Massachusetts', 'Pennsylvania', 'Maryland', 'DC', 'Virginia', 'Delaware', 'Rhode Island', 'New Hampshire', 'Vermont', 'Maine'],\n 'west-coast': ['California', 'Washington', 'Oregon', 'Hawaii'],\n 'south': ['Texas', 'Florida', 'Georgia', 'North Carolina', 'South Carolina', 'Tennessee', 'Alabama', 'Louisiana', 'Mississippi', 'Arkansas', 'Kentucky', 'West Virginia', 'Oklahoma'],\n 'mountain-west': ['Colorado', 'Utah', 'Arizona', 'Nevada', 'New Mexico', 'Idaho', 'Montana', 'Wyoming', 'Alaska'],\n}\n\nconst VALID_REGIONS = Object.keys(REGION_MAP)\n\nconst PROMPT_TEMPLATE = `Given this investor's profile, determine their headquarters location.\nReturn ONLY a JSON object: {\"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- state must be the full US state name (e.g., \"California\", \"Illinois\", \"New York\")\n- region must be one of: midwest, west-coast, east-coast, south, mountain-west\n- If you cannot determine location, return {\"city\": null, \"state\": null, \"region\": null}\n\nRegion mapping:\n- midwest: Illinois, Wisconsin, Indiana, Michigan, Ohio, Minnesota, Iowa, Missouri, Nebraska, Kansas, North Dakota, South Dakota\n- east-coast: New York, New Jersey, Connecticut, Massachusetts, Pennsylvania, Maryland, DC, Virginia, Delaware, Rhode Island, New Hampshire, Vermont, Maine\n- west-coast: California, Washington, Oregon, Hawaii\n- south: Texas, Florida, Georgia, North Carolina, South Carolina, Tennessee, Alabama, Louisiana, Mississippi, Arkansas, Kentucky, West Virginia, Oklahoma\n- mountain-west: Colorado, Utah, Arizona, Nevada, New Mexico, Idaho, Montana, Wyoming, Alaska\n\nInvestor: {canonical_name}\nWebsite: {website}\nProfile: {search_profile}`\n\n// ── Supabase helpers ────────────────────────────────────────────────────────\n\nconst supabaseHeaders = {\n 'apikey': SUPABASE_KEY,\n 'Authorization': `Bearer ${SUPABASE_KEY}`,\n 'Content-Type': 'application/json',\n}\n\nasync function fetchInvestorsMissingLocation() {\n const allInvestors = []\n let offset = 0\n const pageSize = 1000\n\n while (true) {\n const url = `${SUPABASE_URL}/rest/v1/investors?hq_state=is.null&hq_country=eq.USA&select=id,canonical_name,website,description,search_profile&offset=${offset}&limit=${pageSize}`\n const res = await fetch(url, { headers: supabaseHeaders })\n\n if (!res.ok) {\n throw new Error(`Supabase fetch failed: ${res.status} ${await res.text()}`)\n }\n\n const batch = await res.json()\n allInvestors.push(...batch)\n\n if (batch.length < pageSize) break\n offset += pageSize\n }\n\n return allInvestors\n}\n\nasync function updateInvestorPATCH(id, city, state, region) {\n const body = {}\n if (city !== null) body.hq_city = city\n if (state !== null) body.hq_state = state\n if (region !== null) body.hq_region = region\n\n if (Object.keys(body).length === 0) return { ok: true, skipped: true }\n\n const url = `${SUPABASE_URL}/rest/v1/investors?id=eq.${id}`\n const res = await fetch(url, {\n method: 'PATCH',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify(body),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestorRPC(id, city, state, region) {\n const url = `${SUPABASE_URL}/rest/v1/rpc/update_investor_location`\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n ...supabaseHeaders,\n 'Prefer': 'return=minimal',\n },\n body: JSON.stringify({\n investor_id: id,\n new_city: city,\n new_state: state,\n new_region: region,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n return { ok: false, error: errText, status: res.status }\n }\n return { ok: true }\n}\n\nasync function updateInvestor(id, city, state, region) {\n if (USE_RPC) {\n return updateInvestorRPC(id, city, state, region)\n }\n return updateInvestorPATCH(id, city, state, region)\n}\n\n// ── DeepSeek AI ─────────────────────────────────────────────────────────────\n\nasync function extractLocation(investor) {\n const prompt = PROMPT_TEMPLATE\n .replace('{canonical_name}', investor.canonical_name || 'Unknown')\n .replace('{website}', investor.website || 'N/A')\n .replace('{search_profile}', (investor.search_profile || investor.description || 'N/A').slice(0, 2000))\n\n try {\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: MODEL,\n messages: [{ role: 'user', content: prompt }],\n max_tokens: 150,\n temperature: 0.2,\n }),\n })\n\n if (!res.ok) {\n const errText = await res.text()\n throw new Error(`DeepSeek API error ${res.status}: ${errText}`)\n }\n\n const data = await res.json()\n const raw = data.choices[0].message.content.trim()\n const jsonStr = raw.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/i, '')\n const parsed = JSON.parse(jsonStr)\n\n // Validate\n const city = typeof parsed.city === 'string' && parsed.city.length > 0 ? parsed.city : null\n const state = typeof parsed.state === 'string' && parsed.state.length > 0 ? parsed.state : null\n let region = typeof parsed.region === 'string' ? parsed.region.toLowerCase() : null\n if (region && !VALID_REGIONS.includes(region)) region = null\n\n // If we have a state but no region, derive it\n if (state && !region) {\n for (const [r, states] of Object.entries(REGION_MAP)) {\n if (states.some(s => s.toLowerCase() === state.toLowerCase())) {\n region = r\n break\n }\n }\n }\n\n return { success: true, city, state, region }\n } catch (error) {\n return { success: false, error: error.message, city: null, state: null, region: null }\n }\n}\n\n// ── Concurrency helper ──────────────────────────────────────────────────────\n\nasync function processInBatches(items, batchSize, delayMs, fn) {\n const results = []\n for (let i = 0; i < items.length; i += batchSize) {\n const batch = items.slice(i, i + batchSize)\n const batchResults = await Promise.all(batch.map(fn))\n results.push(...batchResults)\n if (i + batchSize < items.length) {\n await new Promise(r => setTimeout(r, delayMs))\n }\n }\n return results\n}\n\n// ── Main ────────────────────────────────────────────────────────────────────\n\nasync function main() {\n console.log('=== Investor Location Backfill ===\\n')\n\n // Step 1: Fetch investors missing location\n console.log('Fetching investors with missing hq_state (USA only)...')\n const investors = await fetchInvestorsMissingLocation()\n console.log(`Found ${investors.length} investors to process.\\n`)\n\n if (investors.length === 0) {\n console.log('Nothing to do. Exiting.')\n return\n }\n\n // Step 2: Extract locations via DeepSeek\n console.log(`Extracting locations via DeepSeek (concurrency=${CONCURRENCY})...\\n`)\n\n const results = []\n let processed = 0\n let locationsFound = 0\n let locationsFailed = 0\n let updateErrors = 0\n\n const allResults = await processInBatches(investors, CONCURRENCY, BATCH_DELAY_MS, async (inv) => {\n const loc = await extractLocation(inv)\n processed++\n\n if (processed % 50 === 0 || processed === investors.length) {\n console.log(` Progress: ${processed}/${investors.length} processed (${locationsFound} locations found so far)`)\n }\n\n const result = {\n id: inv.id,\n canonical_name: inv.canonical_name,\n extracted: loc,\n updated: false,\n }\n\n if (loc.success && (loc.city || loc.state || loc.region)) {\n locationsFound++\n\n // Step 3: Update in Supabase\n const updateResult = await updateInvestor(inv.id, loc.city, loc.state, loc.region)\n\n if (!updateResult.ok) {\n // If PATCH fails due to RLS, switch to RPC\n if (!USE_RPC && (updateResult.status === 403 || (updateResult.error && updateResult.error.includes('policy')))) {\n console.log(`\\n PATCH blocked by RLS (status ${updateResult.status}). Switching to RPC mode...\\n`)\n USE_RPC = true\n\n // Retry this one with RPC\n const retryResult = await updateInvestorRPC(inv.id, loc.city, loc.state, loc.region)\n if (retryResult.ok) {\n result.updated = true\n } else {\n console.log(` RPC also failed for ${inv.canonical_name}: ${retryResult.error}`)\n updateErrors++\n result.updateError = retryResult.error\n }\n } else {\n updateErrors++\n result.updateError = updateResult.error\n }\n } else {\n result.updated = true\n }\n } else {\n locationsFailed++\n }\n\n return result\n })\n\n // Step 4: Save results\n const output = {\n timestamp: new Date().toISOString(),\n summary: {\n total: investors.length,\n locationsFound,\n stillNull: locationsFailed,\n updateErrors,\n useRPC: USE_RPC,\n },\n results: allResults,\n }\n\n fs.writeFileSync(RESULTS_PATH, JSON.stringify(output, null, 2))\n console.log(`\\nResults saved to ${RESULTS_PATH}`)\n\n // Step 5: Summary\n console.log('\\n=== SUMMARY ===')\n console.log(`Total processed: ${investors.length}`)\n console.log(`Locations found: ${locationsFound}`)\n console.log(`Still null: ${locationsFailed}`)\n console.log(`Update errors: ${updateErrors}`)\n console.log(`Update method: ${USE_RPC ? 'RPC' : 'PATCH'}`)\n console.log('================\\n')\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err)\n process.exit(1)\n})\n"}}, "tools/investor-harvester/src/fix-countries.js": {"f5ca936971b96054f07d4b5ae007ed67e3399c3f": {"t": 1785746703.4149702, "n": 8321, "h": "79f570d295b556ae", "c": "/**\n * fix-countries.js\n *\n * Fixes ~66 investors tagged hq_country='USA' that are actually based in other countries.\n * Uses DeepSeek to analyze search_profile text and determine actual HQ country/city.\n * Then updates via Supabase REST RPCs.\n */\n\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co';\nconst SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E';\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4';\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions';\n\nconst CONCURRENCY = 5;\nconst BATCH_DELAY_MS = 200;\n\nasync function fetchMistaggedInvestors() {\n // Build the OR filter for search_profile keywords\n const keywords = [\n 'Europe', 'European', 'Dutch', 'Israel', 'London', 'Berlin',\n 'across Europe', 'Irish', 'Ireland', 'UK ', 'United Kingdom',\n 'Nordic', 'Scandinavian', 'Paris', 'Singapore', 'Hong Kong',\n 'Mexico', 'India', 'Chinese', 'Japan', 'Korean'\n ];\n\n const orFilters = keywords.map(kw => `search_profile.ilike.*${kw}*`).join(',');\n\n const url = new URL(`${SUPABASE_URL}/rest/v1/investors`);\n url.searchParams.set('hq_country', 'eq.USA');\n url.searchParams.set('hq_state', 'is.null');\n url.searchParams.set('or', `(${orFilters})`);\n url.searchParams.set('select', 'id,canonical_name,website,search_profile,description');\n\n const res = await fetch(url.toString(), {\n headers: {\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to fetch investors: ${res.status} ${text}`);\n }\n\n return res.json();\n}\n\nasync function classifyWithDeepSeek(investor) {\n const prompt = `Given this investor's profile, determine their actual headquarters country and city. Return ONLY JSON: {\"country\": \"...\", \"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- country: actual country name (e.g., 'UK', 'Germany', 'Israel', 'Singapore', 'France')\n- city: city name or null\n- state: state name or null (only for US investors)\n- region: one of: midwest, west-coast, east-coast, south, mountain-west, europe, asia, africa, latam, mena, canada, oceania\n- If they are ACTUALLY US-based, return {\"country\": \"USA\", \"city\": null, \"state\": null, \"region\": null}\n\nInvestor: ${investor.canonical_name}\nWebsite: ${investor.website || 'N/A'}\nProfile: ${investor.search_profile || investor.description || 'N/A'}`;\n\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a data classification assistant. Return only valid JSON, no markdown fences, no extra text.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.1,\n max_tokens: 200,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`DeepSeek API error: ${res.status} ${text}`);\n }\n\n const data = await res.json();\n const content = data.choices[0].message.content.trim();\n\n // Parse JSON, handling potential markdown fences\n let cleaned = content;\n if (cleaned.startsWith('```')) {\n cleaned = cleaned.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '');\n }\n\n try {\n return JSON.parse(cleaned);\n } catch (e) {\n console.error(` Failed to parse DeepSeek response for ${investor.canonical_name}: ${content}`);\n return null;\n }\n}\n\nasync function updateInvestorCountry(investorId, newCountry) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_country`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_country: newCountry,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update country for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nasync function updateInvestorLocation(investorId, city, state, region) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_location`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_city: city || null,\n new_state: state || null,\n new_region: region || null,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update location for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nasync function processBatch(batch, results) {\n const promises = batch.map(async (investor) => {\n try {\n const classification = await classifyWithDeepSeek(investor);\n if (!classification) {\n results.errors.push(investor.canonical_name);\n return;\n }\n\n results.processed++;\n\n if (classification.country && classification.country !== 'USA') {\n // Update country\n await updateInvestorCountry(investor.id, classification.country);\n // Update location (city, state, region)\n await updateInvestorLocation(\n investor.id,\n classification.city,\n classification.state,\n classification.region\n );\n\n results.reclassified++;\n const countryKey = classification.country;\n results.byCountry[countryKey] = (results.byCountry[countryKey] || 0) + 1;\n\n console.log(` RECLASSIFIED: ${investor.canonical_name} -> ${classification.country} (${classification.city || 'no city'}, region: ${classification.region || 'none'})`);\n } else {\n results.keptUSA++;\n console.log(` KEPT USA: ${investor.canonical_name}`);\n }\n } catch (err) {\n console.error(` ERROR processing ${investor.canonical_name}: ${err.message}`);\n results.errors.push(investor.canonical_name);\n }\n });\n\n await Promise.all(promises);\n}\n\nasync function main() {\n console.log('=== Investor Country Fix Script ===\\n');\n\n // Step 1: Fetch mistagged investors\n console.log('Fetching investors with hq_country=USA, hq_state=NULL, and non-US keywords in profile...');\n const investors = await fetchMistaggedInvestors();\n console.log(`Found ${investors.length} investors to check.\\n`);\n\n if (investors.length === 0) {\n console.log('No investors to process. Exiting.');\n return;\n }\n\n // Step 2: Process in batches of CONCURRENCY\n const results = {\n processed: 0,\n reclassified: 0,\n keptUSA: 0,\n errors: [],\n byCountry: {},\n };\n\n for (let i = 0; i < investors.length; i += CONCURRENCY) {\n const batch = investors.slice(i, i + CONCURRENCY);\n const batchNum = Math.floor(i / CONCURRENCY) + 1;\n const totalBatches = Math.ceil(investors.length / CONCURRENCY);\n\n if (i % 20 === 0 || i === 0) {\n console.log(`\\n--- Progress: ${i}/${investors.length} investors processed (batch ${batchNum}/${totalBatches}) ---`);\n }\n\n await processBatch(batch, results);\n\n if (i + CONCURRENCY < investors.length) {\n await sleep(BATCH_DELAY_MS);\n }\n }\n\n // Step 3: Print summary\n console.log('\\n\\n========== SUMMARY ==========');\n console.log(`Total fetched: ${investors.length}`);\n console.log(`Total processed: ${results.processed}`);\n console.log(`Reclassified: ${results.reclassified}`);\n console.log(`Kept as USA: ${results.keptUSA}`);\n console.log(`Errors: ${results.errors.length}`);\n\n if (results.errors.length > 0) {\n console.log(`\\nError investors: ${results.errors.join(', ')}`);\n }\n\n console.log('\\n--- By Country Breakdown ---');\n const sorted = Object.entries(results.byCountry).sort((a, b) => b[1] - a[1]);\n for (const [country, count] of sorted) {\n console.log(` ${country}: ${count}`);\n }\n console.log('=============================\\n');\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n"}, "d8888e64b5c232e6e53e41dac148e23d3b6f2c3f": {"t": 1785767083.986915, "n": 8321, "h": "79f570d295b556ae", "c": "/**\n * fix-countries.js\n *\n * Fixes ~66 investors tagged hq_country='USA' that are actually based in other countries.\n * Uses DeepSeek to analyze search_profile text and determine actual HQ country/city.\n * Then updates via Supabase REST RPCs.\n */\n\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co';\nconst SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E';\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4';\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions';\n\nconst CONCURRENCY = 5;\nconst BATCH_DELAY_MS = 200;\n\nasync function fetchMistaggedInvestors() {\n // Build the OR filter for search_profile keywords\n const keywords = [\n 'Europe', 'European', 'Dutch', 'Israel', 'London', 'Berlin',\n 'across Europe', 'Irish', 'Ireland', 'UK ', 'United Kingdom',\n 'Nordic', 'Scandinavian', 'Paris', 'Singapore', 'Hong Kong',\n 'Mexico', 'India', 'Chinese', 'Japan', 'Korean'\n ];\n\n const orFilters = keywords.map(kw => `search_profile.ilike.*${kw}*`).join(',');\n\n const url = new URL(`${SUPABASE_URL}/rest/v1/investors`);\n url.searchParams.set('hq_country', 'eq.USA');\n url.searchParams.set('hq_state', 'is.null');\n url.searchParams.set('or', `(${orFilters})`);\n url.searchParams.set('select', 'id,canonical_name,website,search_profile,description');\n\n const res = await fetch(url.toString(), {\n headers: {\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to fetch investors: ${res.status} ${text}`);\n }\n\n return res.json();\n}\n\nasync function classifyWithDeepSeek(investor) {\n const prompt = `Given this investor's profile, determine their actual headquarters country and city. Return ONLY JSON: {\"country\": \"...\", \"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- country: actual country name (e.g., 'UK', 'Germany', 'Israel', 'Singapore', 'France')\n- city: city name or null\n- state: state name or null (only for US investors)\n- region: one of: midwest, west-coast, east-coast, south, mountain-west, europe, asia, africa, latam, mena, canada, oceania\n- If they are ACTUALLY US-based, return {\"country\": \"USA\", \"city\": null, \"state\": null, \"region\": null}\n\nInvestor: ${investor.canonical_name}\nWebsite: ${investor.website || 'N/A'}\nProfile: ${investor.search_profile || investor.description || 'N/A'}`;\n\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a data classification assistant. Return only valid JSON, no markdown fences, no extra text.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.1,\n max_tokens: 200,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`DeepSeek API error: ${res.status} ${text}`);\n }\n\n const data = await res.json();\n const content = data.choices[0].message.content.trim();\n\n // Parse JSON, handling potential markdown fences\n let cleaned = content;\n if (cleaned.startsWith('```')) {\n cleaned = cleaned.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '');\n }\n\n try {\n return JSON.parse(cleaned);\n } catch (e) {\n console.error(` Failed to parse DeepSeek response for ${investor.canonical_name}: ${content}`);\n return null;\n }\n}\n\nasync function updateInvestorCountry(investorId, newCountry) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_country`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_country: newCountry,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update country for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nasync function updateInvestorLocation(investorId, city, state, region) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_location`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_city: city || null,\n new_state: state || null,\n new_region: region || null,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update location for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nasync function processBatch(batch, results) {\n const promises = batch.map(async (investor) => {\n try {\n const classification = await classifyWithDeepSeek(investor);\n if (!classification) {\n results.errors.push(investor.canonical_name);\n return;\n }\n\n results.processed++;\n\n if (classification.country && classification.country !== 'USA') {\n // Update country\n await updateInvestorCountry(investor.id, classification.country);\n // Update location (city, state, region)\n await updateInvestorLocation(\n investor.id,\n classification.city,\n classification.state,\n classification.region\n );\n\n results.reclassified++;\n const countryKey = classification.country;\n results.byCountry[countryKey] = (results.byCountry[countryKey] || 0) + 1;\n\n console.log(` RECLASSIFIED: ${investor.canonical_name} -> ${classification.country} (${classification.city || 'no city'}, region: ${classification.region || 'none'})`);\n } else {\n results.keptUSA++;\n console.log(` KEPT USA: ${investor.canonical_name}`);\n }\n } catch (err) {\n console.error(` ERROR processing ${investor.canonical_name}: ${err.message}`);\n results.errors.push(investor.canonical_name);\n }\n });\n\n await Promise.all(promises);\n}\n\nasync function main() {\n console.log('=== Investor Country Fix Script ===\\n');\n\n // Step 1: Fetch mistagged investors\n console.log('Fetching investors with hq_country=USA, hq_state=NULL, and non-US keywords in profile...');\n const investors = await fetchMistaggedInvestors();\n console.log(`Found ${investors.length} investors to check.\\n`);\n\n if (investors.length === 0) {\n console.log('No investors to process. Exiting.');\n return;\n }\n\n // Step 2: Process in batches of CONCURRENCY\n const results = {\n processed: 0,\n reclassified: 0,\n keptUSA: 0,\n errors: [],\n byCountry: {},\n };\n\n for (let i = 0; i < investors.length; i += CONCURRENCY) {\n const batch = investors.slice(i, i + CONCURRENCY);\n const batchNum = Math.floor(i / CONCURRENCY) + 1;\n const totalBatches = Math.ceil(investors.length / CONCURRENCY);\n\n if (i % 20 === 0 || i === 0) {\n console.log(`\\n--- Progress: ${i}/${investors.length} investors processed (batch ${batchNum}/${totalBatches}) ---`);\n }\n\n await processBatch(batch, results);\n\n if (i + CONCURRENCY < investors.length) {\n await sleep(BATCH_DELAY_MS);\n }\n }\n\n // Step 3: Print summary\n console.log('\\n\\n========== SUMMARY ==========');\n console.log(`Total fetched: ${investors.length}`);\n console.log(`Total processed: ${results.processed}`);\n console.log(`Reclassified: ${results.reclassified}`);\n console.log(`Kept as USA: ${results.keptUSA}`);\n console.log(`Errors: ${results.errors.length}`);\n\n if (results.errors.length > 0) {\n console.log(`\\nError investors: ${results.errors.join(', ')}`);\n }\n\n console.log('\\n--- By Country Breakdown ---');\n const sorted = Object.entries(results.byCountry).sort((a, b) => b[1] - a[1]);\n for (const [country, count] of sorted) {\n console.log(` ${country}: ${count}`);\n }\n console.log('=============================\\n');\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n"}, "06098332267a6a2bbdee585f61c04c6b89d71023": {"t": 1786067874.4801393, "n": 8321, "h": "79f570d295b556ae", "c": "/**\n * fix-countries.js\n *\n * Fixes ~66 investors tagged hq_country='USA' that are actually based in other countries.\n * Uses DeepSeek to analyze search_profile text and determine actual HQ country/city.\n * Then updates via Supabase REST RPCs.\n */\n\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co';\nconst SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E';\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4';\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions';\n\nconst CONCURRENCY = 5;\nconst BATCH_DELAY_MS = 200;\n\nasync function fetchMistaggedInvestors() {\n // Build the OR filter for search_profile keywords\n const keywords = [\n 'Europe', 'European', 'Dutch', 'Israel', 'London', 'Berlin',\n 'across Europe', 'Irish', 'Ireland', 'UK ', 'United Kingdom',\n 'Nordic', 'Scandinavian', 'Paris', 'Singapore', 'Hong Kong',\n 'Mexico', 'India', 'Chinese', 'Japan', 'Korean'\n ];\n\n const orFilters = keywords.map(kw => `search_profile.ilike.*${kw}*`).join(',');\n\n const url = new URL(`${SUPABASE_URL}/rest/v1/investors`);\n url.searchParams.set('hq_country', 'eq.USA');\n url.searchParams.set('hq_state', 'is.null');\n url.searchParams.set('or', `(${orFilters})`);\n url.searchParams.set('select', 'id,canonical_name,website,search_profile,description');\n\n const res = await fetch(url.toString(), {\n headers: {\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to fetch investors: ${res.status} ${text}`);\n }\n\n return res.json();\n}\n\nasync function classifyWithDeepSeek(investor) {\n const prompt = `Given this investor's profile, determine their actual headquarters country and city. Return ONLY JSON: {\"country\": \"...\", \"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- country: actual country name (e.g., 'UK', 'Germany', 'Israel', 'Singapore', 'France')\n- city: city name or null\n- state: state name or null (only for US investors)\n- region: one of: midwest, west-coast, east-coast, south, mountain-west, europe, asia, africa, latam, mena, canada, oceania\n- If they are ACTUALLY US-based, return {\"country\": \"USA\", \"city\": null, \"state\": null, \"region\": null}\n\nInvestor: ${investor.canonical_name}\nWebsite: ${investor.website || 'N/A'}\nProfile: ${investor.search_profile || investor.description || 'N/A'}`;\n\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a data classification assistant. Return only valid JSON, no markdown fences, no extra text.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.1,\n max_tokens: 200,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`DeepSeek API error: ${res.status} ${text}`);\n }\n\n const data = await res.json();\n const content = data.choices[0].message.content.trim();\n\n // Parse JSON, handling potential markdown fences\n let cleaned = content;\n if (cleaned.startsWith('```')) {\n cleaned = cleaned.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '');\n }\n\n try {\n return JSON.parse(cleaned);\n } catch (e) {\n console.error(` Failed to parse DeepSeek response for ${investor.canonical_name}: ${content}`);\n return null;\n }\n}\n\nasync function updateInvestorCountry(investorId, newCountry) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_country`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_country: newCountry,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update country for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nasync function updateInvestorLocation(investorId, city, state, region) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_location`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_city: city || null,\n new_state: state || null,\n new_region: region || null,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update location for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nasync function processBatch(batch, results) {\n const promises = batch.map(async (investor) => {\n try {\n const classification = await classifyWithDeepSeek(investor);\n if (!classification) {\n results.errors.push(investor.canonical_name);\n return;\n }\n\n results.processed++;\n\n if (classification.country && classification.country !== 'USA') {\n // Update country\n await updateInvestorCountry(investor.id, classification.country);\n // Update location (city, state, region)\n await updateInvestorLocation(\n investor.id,\n classification.city,\n classification.state,\n classification.region\n );\n\n results.reclassified++;\n const countryKey = classification.country;\n results.byCountry[countryKey] = (results.byCountry[countryKey] || 0) + 1;\n\n console.log(` RECLASSIFIED: ${investor.canonical_name} -> ${classification.country} (${classification.city || 'no city'}, region: ${classification.region || 'none'})`);\n } else {\n results.keptUSA++;\n console.log(` KEPT USA: ${investor.canonical_name}`);\n }\n } catch (err) {\n console.error(` ERROR processing ${investor.canonical_name}: ${err.message}`);\n results.errors.push(investor.canonical_name);\n }\n });\n\n await Promise.all(promises);\n}\n\nasync function main() {\n console.log('=== Investor Country Fix Script ===\\n');\n\n // Step 1: Fetch mistagged investors\n console.log('Fetching investors with hq_country=USA, hq_state=NULL, and non-US keywords in profile...');\n const investors = await fetchMistaggedInvestors();\n console.log(`Found ${investors.length} investors to check.\\n`);\n\n if (investors.length === 0) {\n console.log('No investors to process. Exiting.');\n return;\n }\n\n // Step 2: Process in batches of CONCURRENCY\n const results = {\n processed: 0,\n reclassified: 0,\n keptUSA: 0,\n errors: [],\n byCountry: {},\n };\n\n for (let i = 0; i < investors.length; i += CONCURRENCY) {\n const batch = investors.slice(i, i + CONCURRENCY);\n const batchNum = Math.floor(i / CONCURRENCY) + 1;\n const totalBatches = Math.ceil(investors.length / CONCURRENCY);\n\n if (i % 20 === 0 || i === 0) {\n console.log(`\\n--- Progress: ${i}/${investors.length} investors processed (batch ${batchNum}/${totalBatches}) ---`);\n }\n\n await processBatch(batch, results);\n\n if (i + CONCURRENCY < investors.length) {\n await sleep(BATCH_DELAY_MS);\n }\n }\n\n // Step 3: Print summary\n console.log('\\n\\n========== SUMMARY ==========');\n console.log(`Total fetched: ${investors.length}`);\n console.log(`Total processed: ${results.processed}`);\n console.log(`Reclassified: ${results.reclassified}`);\n console.log(`Kept as USA: ${results.keptUSA}`);\n console.log(`Errors: ${results.errors.length}`);\n\n if (results.errors.length > 0) {\n console.log(`\\nError investors: ${results.errors.join(', ')}`);\n }\n\n console.log('\\n--- By Country Breakdown ---');\n const sorted = Object.entries(results.byCountry).sort((a, b) => b[1] - a[1]);\n for (const [country, count] of sorted) {\n console.log(` ${country}: ${count}`);\n }\n console.log('=============================\\n');\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n"}, "be95a9d7fbfaae474b70eadfca5a956cf6e99169": {"t": 1786068488.8244326, "n": 8321, "h": "79f570d295b556ae", "c": "/**\n * fix-countries.js\n *\n * Fixes ~66 investors tagged hq_country='USA' that are actually based in other countries.\n * Uses DeepSeek to analyze search_profile text and determine actual HQ country/city.\n * Then updates via Supabase REST RPCs.\n */\n\nconst SUPABASE_URL = 'https://fbgxeinarhbrqatrsuoj.supabase.co';\nconst SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZiZ3hlaW5hcmhicnFhdHJzdW9qIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYzNTIyNjIsImV4cCI6MjA4MTkyODI2Mn0.k6yRcQ60OONig97VQZ-UJdmC49ijEm7kskP_2qtaW1E';\nconst DEEPSEEK_API_KEY = 'sk-d928e785a8c64d5580594c9d60c56de4';\nconst DEEPSEEK_URL = 'https://api.deepseek.com/chat/completions';\n\nconst CONCURRENCY = 5;\nconst BATCH_DELAY_MS = 200;\n\nasync function fetchMistaggedInvestors() {\n // Build the OR filter for search_profile keywords\n const keywords = [\n 'Europe', 'European', 'Dutch', 'Israel', 'London', 'Berlin',\n 'across Europe', 'Irish', 'Ireland', 'UK ', 'United Kingdom',\n 'Nordic', 'Scandinavian', 'Paris', 'Singapore', 'Hong Kong',\n 'Mexico', 'India', 'Chinese', 'Japan', 'Korean'\n ];\n\n const orFilters = keywords.map(kw => `search_profile.ilike.*${kw}*`).join(',');\n\n const url = new URL(`${SUPABASE_URL}/rest/v1/investors`);\n url.searchParams.set('hq_country', 'eq.USA');\n url.searchParams.set('hq_state', 'is.null');\n url.searchParams.set('or', `(${orFilters})`);\n url.searchParams.set('select', 'id,canonical_name,website,search_profile,description');\n\n const res = await fetch(url.toString(), {\n headers: {\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to fetch investors: ${res.status} ${text}`);\n }\n\n return res.json();\n}\n\nasync function classifyWithDeepSeek(investor) {\n const prompt = `Given this investor's profile, determine their actual headquarters country and city. Return ONLY JSON: {\"country\": \"...\", \"city\": \"...\", \"state\": \"...\", \"region\": \"...\"}\n- country: actual country name (e.g., 'UK', 'Germany', 'Israel', 'Singapore', 'France')\n- city: city name or null\n- state: state name or null (only for US investors)\n- region: one of: midwest, west-coast, east-coast, south, mountain-west, europe, asia, africa, latam, mena, canada, oceania\n- If they are ACTUALLY US-based, return {\"country\": \"USA\", \"city\": null, \"state\": null, \"region\": null}\n\nInvestor: ${investor.canonical_name}\nWebsite: ${investor.website || 'N/A'}\nProfile: ${investor.search_profile || investor.description || 'N/A'}`;\n\n const res = await fetch(DEEPSEEK_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: 'You are a data classification assistant. Return only valid JSON, no markdown fences, no extra text.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.1,\n max_tokens: 200,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`DeepSeek API error: ${res.status} ${text}`);\n }\n\n const data = await res.json();\n const content = data.choices[0].message.content.trim();\n\n // Parse JSON, handling potential markdown fences\n let cleaned = content;\n if (cleaned.startsWith('```')) {\n cleaned = cleaned.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '');\n }\n\n try {\n return JSON.parse(cleaned);\n } catch (e) {\n console.error(` Failed to parse DeepSeek response for ${investor.canonical_name}: ${content}`);\n return null;\n }\n}\n\nasync function updateInvestorCountry(investorId, newCountry) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_country`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_country: newCountry,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update country for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nasync function updateInvestorLocation(investorId, city, state, region) {\n const res = await fetch(`${SUPABASE_URL}/rest/v1/rpc/update_investor_location`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'apikey': SUPABASE_ANON_KEY,\n 'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,\n },\n body: JSON.stringify({\n investor_id: investorId,\n new_city: city || null,\n new_state: state || null,\n new_region: region || null,\n }),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Failed to update location for ${investorId}: ${res.status} ${text}`);\n }\n}\n\nfunction sleep(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nasync function processBatch(batch, results) {\n const promises = batch.map(async (investor) => {\n try {\n const classification = await classifyWithDeepSeek(investor);\n if (!classification) {\n results.errors.push(investor.canonical_name);\n return;\n }\n\n results.processed++;\n\n if (classification.country && classification.country !== 'USA') {\n // Update country\n await updateInvestorCountry(investor.id, classification.country);\n // Update location (city, state, region)\n await updateInvestorLocation(\n investor.id,\n classification.city,\n classification.state,\n classification.region\n );\n\n results.reclassified++;\n const countryKey = classification.country;\n results.byCountry[countryKey] = (results.byCountry[countryKey] || 0) + 1;\n\n console.log(` RECLASSIFIED: ${investor.canonical_name} -> ${classification.country} (${classification.city || 'no city'}, region: ${classification.region || 'none'})`);\n } else {\n results.keptUSA++;\n console.log(` KEPT USA: ${investor.canonical_name}`);\n }\n } catch (err) {\n console.error(` ERROR processing ${investor.canonical_name}: ${err.message}`);\n results.errors.push(investor.canonical_name);\n }\n });\n\n await Promise.all(promises);\n}\n\nasync function main() {\n console.log('=== Investor Country Fix Script ===\\n');\n\n // Step 1: Fetch mistagged investors\n console.log('Fetching investors with hq_country=USA, hq_state=NULL, and non-US keywords in profile...');\n const investors = await fetchMistaggedInvestors();\n console.log(`Found ${investors.length} investors to check.\\n`);\n\n if (investors.length === 0) {\n console.log('No investors to process. Exiting.');\n return;\n }\n\n // Step 2: Process in batches of CONCURRENCY\n const results = {\n processed: 0,\n reclassified: 0,\n keptUSA: 0,\n errors: [],\n byCountry: {},\n };\n\n for (let i = 0; i < investors.length; i += CONCURRENCY) {\n const batch = investors.slice(i, i + CONCURRENCY);\n const batchNum = Math.floor(i / CONCURRENCY) + 1;\n const totalBatches = Math.ceil(investors.length / CONCURRENCY);\n\n if (i % 20 === 0 || i === 0) {\n console.log(`\\n--- Progress: ${i}/${investors.length} investors processed (batch ${batchNum}/${totalBatches}) ---`);\n }\n\n await processBatch(batch, results);\n\n if (i + CONCURRENCY < investors.length) {\n await sleep(BATCH_DELAY_MS);\n }\n }\n\n // Step 3: Print summary\n console.log('\\n\\n========== SUMMARY ==========');\n console.log(`Total fetched: ${investors.length}`);\n console.log(`Total processed: ${results.processed}`);\n console.log(`Reclassified: ${results.reclassified}`);\n console.log(`Kept as USA: ${results.keptUSA}`);\n console.log(`Errors: ${results.errors.length}`);\n\n if (results.errors.length > 0) {\n console.log(`\\nError investors: ${results.errors.join(', ')}`);\n }\n\n console.log('\\n--- By Country Breakdown ---');\n const sorted = Object.entries(results.byCountry).sort((a, b) => b[1] - a[1]);\n for (const [country, count] of sorted) {\n console.log(` ${country}: ${count}`);\n }\n console.log('=============================\\n');\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n"}}}} |