Is PolyPoll - Create Prediction Markets from Tweets & News safe?
PolyPoll is high risk. When you click PolyPoll's parrot button on Twitter/X, the extension reads the selected tweet's text, author, status URL, tweet ID, and timestamp, then posts these fields to PolyPoll's backend for market-suggestion generation.โฆ
Who publishes itsamatcrispy - no other listings under this identity
samatcrispy - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Tweet Data Sent for AI Market Suggestions
When you click PolyPoll's parrot button on Twitter/X, the extension reads the selected tweet's text, author, status URL, tweet ID, and timestamp, then posts these fields to PolyPoll's backend for market-suggestion generation.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You click the PolyPoll parrot button next to a Twitter/X post.
The button is injected into the tweet action bar by the content script.
The extension reads the tweet fields from the page and sends them to PolyPoll for market-suggestion generation.
The fields include tweet text, author, URL, tweet ID, and timestamp context.
- Tweet URLhttps://x.com/exampleuser/status/1812865104532283480 (illustrative)
Identifies the exact post you selected for a market suggestion.
- Tweet textOur product launch is moving to Friday after the final security review. (illustrative)
Includes the text of the post you selected.
- Tweet authorExample User (illustrative)
Associates the selected post with the account name shown on the page.
- Tweet ID and time1812865104532283480 at 2026-07-14T15:32:00.000Z (illustrative)
Adds stable context for the selected post and when it appeared.
- Content-Type
- application/json
Shipped source path from tweet button to backend POST
Readable shipped source: tweet button click
content.js parrotButton.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); log(LOG_LEVELS.INFO, 'UI', '๐ฆ Parrot button clicked - opening bet modal'); // Extract tweet data and set it as article data const tweetData = extractTweetData(tweetElement); polypollContentState.articleData = tweetData; log(LOG_LEVELS.DEBUG, 'UI', 'Tweet data extracted:', tweetData); // Track parrot button click contentPostHog.trackParrotButtonClicked(tweetData); // Show inline bet modal showBetModal(tweetData); });Readable shipped source: tweet extraction
content.jsfunction extractTweetData(tweetElement) { const data = { url: window.location.href, // Will be updated with specific tweet URL if found hostname: window.location.hostname, title: null, author: null, date: null, summary: null, content: null, tweetId: null, tweetUrl: null }; log(LOG_LEVELS.DEBUG, 'EXTRACT', 'Starting tweet data extraction...'); // Get tweet text - this is the most important part const tweetText = tweetElement.querySelector('[data-testid="tweetText"]'); if (tweetText) { data.content = tweetText.textContent?.trim(); data.title = data.content?.substring(0, 100) + (data.content?.length > 100 ? '...' : ''); log(LOG_LEVELS.DEBUG, 'EXTRACT', `Tweet content: "${data.content}"`); } else { log(LOG_LEVELS.ERROR, 'EXTRACT', 'No tweet text found!'); } // Get author - try multiple selectors let authorElement = tweetElement.querySelector('[data-testid="User-Name"] span') || tweetElement.querySelector('[data-testid="User-Names"] span:first-child') || tweetElement.querySelector('[data-testid="User-Name"]') || tweetElement.querySelector('[data-testid="User-Names"]'); if (authorElement) { data.author = authorElement.textContent?.trim(); log(LOG_LEVELS.DEBUG, 'EXTRACT', `Tweet author: "${data.author}"`); } // Get username for URL construction let username = null; const usernameElement = tweetElement.querySelector('[data-testid="User-Names"] a[href^="/"]'); if (usernameElement) { const href = usernameElement.getAttribute('href'); username = href?.replace('/', ''); // Remove leading slash log(LOG_LEVELS.DEBUG, 'EXTRACT', `Username: "${username}"`); } // Get timestamp const timeElement = tweetElement.querySelector('time'); if (timeElement) { data.date = timeElement.getAttribute('datetime') || timeElement.textContent; log(LOG_LEVELS.DEBUG, 'EXTRACT', `Tweet date: "${data.date}"`); } // Extract tweet ID and construct specific tweet URL // Look for links that contain "/status/" const statusLink = tweetElement.querySelector('a[href*="/status/"]'); if (statusLink) { const href = statusLink.getAttribute('href'); const statusMatch = href.match(/\/status\/(\d+)/); if (statusMatch) { data.tweetId = statusMatch[1]; // Construct proper tweet URL if (username) { data.tweetUrl = `https://x.com/${username}/status/${data.tweetId}`; data.url = data.tweetUrl; // Use specific tweet URL instead of current page URL } else { data.tweetUrl = `https://x.com/i/status/${data.tweetId}`; data.url = data.tweetUrl; } log(LOG_LEVELS.DEBUG, 'EXTRACT', `Tweet ID: ${data.tweetId}`); log(LOG_LEVELS.DEBUG, 'EXTRACT', `Tweet URL: ${data.tweetUrl}`); } } // If we couldn't find a specific tweet URL, check if current page is a tweet if (!data.tweetUrl && window.location.pathname.includes('/status/')) { data.tweetUrl = window.location.href; data.url = data.tweetUrl; const urlMatch = window.location.pathname.match(/\/status\/(\d+)/); if (urlMatch) { data.tweetId = urlMatch[1]; } log(LOG_LEVELS.DEBUG, 'EXTRACT', `Using current page as tweet URL: ${data.tweetUrl}`); } data.summary = data.content?.substring(0, 200) + (data.content?.length > 200 ? '...' : ''); log(LOG_LEVELS.INFO, 'EXTRACT', `Tweet extraction complete - ID: ${data.tweetId}, Author: ${data.author}, Content: ${data.content?.substring(0, 50)}...`); return data;}Readable shipped source: instant suggestion request
content.js const suggestionResponse = await fetch(`${CONFIG.API_BASE_URL}/api/v1/twitter-instant-suggest`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tweet_content: tweetData.content, tweet_author: tweetData.author, tweet_url: tweetData.url || tweetData.tweetUrl }) });Readable shipped source: market creation request
content.js const response = await fetch(`${CONFIG.API_BASE_URL}/api/v1/markets-mvp/create-market-fast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: `Based on this tweet by ${tweetData.author}: "${tweetData.content}"`, user_id: 'twitter_extension', tweet_content: tweetData.content, tweet_author: tweetData.author, tweet_url: tweetData.url || tweetData.tweetUrl, poll_mode: false, fast_mode: true, deploy_on_chain: false, force_duplicate: false }) });Readable shipped source: background Twitter branch
background.jsasync function handleAIPollGeneration(articleData) { log(LOG_LEVELS.DEBUG, 'AI', '๐ค Generating poll with new AI endpoint', { title: articleData?.title?.substring(0, 50), contentLength: articleData?.content?.length }); try { // Check if this is a Twitter/X URL const isTwitter = articleData.url && ( articleData.url.includes('twitter.com') || articleData.url.includes('x.com') ); let endpoint, requestBody; const apiBaseUrl = getApiBaseUrl(); if (isTwitter) { // Use Twitter instant suggest endpoint (ultra-fast) log(LOG_LEVELS.INFO, 'AI', '๐ฆ Detected Twitter/X content - using instant suggest'); endpoint = `${apiBaseUrl}/api/v1/twitter-instant-suggest`; requestBody = { tweet_content: articleData.content || articleData.summary || '', tweet_author: articleData.author || 'Unknown', tweet_url: articleData.url || null, user_id: 'twitter_extension' }; } else { // Use regular article poll endpoint endpoint = `${apiBaseUrl}/api/ai/generate-poll`; requestBody = { title: articleData.title || 'Untitled Article', content: articleData.content || articleData.summary || 'No content available', url: articleData.url || 'https://example.com' }; } log(LOG_LEVELS.DEBUG, 'AI', '๐ก Making request to AI endpoint', { endpoint: endpoint, bodyKeys: Object.keys(requestBody), isTwitter: isTwitter }); const response = await fetch(endpoint, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); log(LOG_LEVELS.ERROR, 'AI', `โ API request failed: ${response.status}`, errorText); throw new Error(`AI API error (${response.status}): ${errorText}`); } const aiResponse = await response.json(); log(LOG_LEVELS.INFO, 'AI', 'โ
AI response received', { hasQuestion: !!aiResponse.question, hasOptions: !!aiResponse.options, optionCount: aiResponse.options?.length || 0, isTwitterPoll: isTwitter }); // Handle Twitter binary poll response format if (isTwitter && aiResponse.options) { // Convert binary poll format to standard format if needed return { question: aiResponse.question, options: aiResponse.options.map(opt => ({ text: opt.text, reasoning: opt.reasoning, expected_percentage: opt.expected_percentage })), metadata: { poll_type: 'binary', confidence_level: aiResponse.confidence_level, resolution_timeframe: aiResponse.resolution_timeframe, ...aiResponse.metadata }, analysis: aiResponse.reasoning || aiResponse.analysis }; } // Return the full AI response with all the rich data for regular polls return { question: aiResponse.question, serial_optimism: aiResponse.serial_optimism, contrarian: aiResponse.contrarian, pessimistic: aiResponse.pessimistic, hottake: aiResponse.hottake, options: aiResponse.options, metadata: aiResponse.metadata, analysis: aiResponse.analysis }; } catch (error) { log(LOG_LEVELS.ERROR, 'AI', 'โ AI poll generation failed', error); throw error; }}- polypoll-backend-frg4.onrender.com
PolyPoll backend endpoint that receives tweet fields for instant market suggestions and market creation.
Article Data Sent for AI Poll Generation
When you click PolyPoll's floating parrot button on an article page, the extension collects the page URL, title, and article text, then posts them to PolyPoll's AI poll-generation endpoint.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You click the PolyPoll parrot button while viewing an article page.
The click opens the poll creation flow inside the page.
The extension reads article fields from the page and sends them to PolyPoll for AI poll generation.
The fields include the current URL, page title, and article text selected by the extractor.
- Article URLhttps://www.reuters.com/world/us/federal-reserve-rate-decision-2026-07-14/ (illustrative)
Shows the exact article page you used to create the poll.
- Page titleFed officials weigh July rate decision as markets watch inflation data (illustrative)
Identifies the headline or title of the article you selected.
- Article textFederal Reserve officials signaled that inflation and labor-market data would guide the next rate decision. (illustrative)
Includes body text from the article, which can reveal what topic you were reading.
- Accept
- application/json
- Content-Type
- application/json
Shipped source path from page click to backend POST
Readable shipped source: article button click
content.js mascot.querySelector('.polly-image').addEventListener('click', () => { log(LOG_LEVELS.INFO, 'UI', '๐ฆ Polly FAB clicked - extracting article content'); // Extract fresh article data from the DOM const articleData = extractArticleData(); polypollContentState.articleData = articleData; log(LOG_LEVELS.DEBUG, 'UI', 'Article data extracted:', articleData); // Use the same modal as Twitter parrot button showBetModal(articleData); });Readable shipped source: article extraction
content.jsfunction extractArticleData() { const { extractor, type } = getExtractor(window.location.hostname); polypollContentState.extractorUsed = type; const data = { url: window.location.href, hostname: window.location.hostname, title: null, author: null, date: null, summary: null, content: null, extractorType: type }; log(LOG_LEVELS.DEBUG, 'EXTRACT', `Starting extraction with ${type} extractor`); // Extract title const titleSelectors = extractor.title.split(', '); for (const selector of titleSelectors) { try { const titleEl = document.querySelector(selector); if (titleEl) { data.title = titleEl.textContent?.trim(); log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Title found with selector: ${selector}`); break; } } catch (e) { log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Title selector failed: ${selector}`); } } // Extract author const authorSelectors = extractor.author.split(', '); for (const selector of authorSelectors) { try { const authorEl = document.querySelector(selector); if (authorEl) { data.author = authorEl.textContent?.trim(); log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Author found with selector: ${selector}`); break; } } catch (e) { log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Author selector failed: ${selector}`); } } // Extract date const dateSelectors = extractor.date.split(', '); for (const selector of dateSelectors) { try { const dateEl = document.querySelector(selector); if (dateEl) { data.date = dateEl.getAttribute('datetime') || dateEl.textContent?.trim(); log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Date found with selector: ${selector}`); break; } } catch (e) { log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Date selector failed: ${selector}`); } } // Extract summary const summarySelectors = extractor.summary.split(', '); for (const selector of summarySelectors) { try { const summaryEl = document.querySelector(selector); if (summaryEl) { data.summary = summaryEl.textContent?.trim(); log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Summary found with selector: ${selector}`); break; } } catch (e) { log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Summary selector failed: ${selector}`); } } // Extract content - aggregate from multiple sources for better context const contentPieces = new Set(); // Use Set to avoid duplicates const MAX_CONTENT_LENGTH = 5000; // Reasonable threshold for AI context let totalLength = 0; // Add WSJ and paywall-specific selectors const wsjSelectors = [ '.wsj-snippet-body', '[data-module-zone="article_snippet"]', '.snippet__body', '.article-wrap', 'div[class*="snippet"]', 'div[class*="article"]', 'div[class*="story"]', '.paywall', '.truncated-content' ]; // First, try to get content from article selectors const articleSelectors = extractor.article.split(', '); const allSelectors = [...articleSelectors, ...wsjSelectors]; for (const selector of allSelectors) { try { const articleEls = document.querySelectorAll(selector); articleEls.forEach(articleEl => { if (totalLength >= MAX_CONTENT_LENGTH) return; // Get all text elements (broader extraction) const textElements = articleEl.querySelectorAll('p, h1, h2, h3, h4, blockquote, li, span'); Array.from(textElements).forEach(el => { const text = el.textContent?.trim(); if (text && text.length > 20 && !contentPieces.has(text)) { // Filter out common non-content text const lowerText = text.toLowerCase(); if (!lowerText.includes('copyright') && !lowerText.includes('all rights reserved') && !lowerText.includes('continue reading') && !lowerText.includes('subscribe') && !lowerText.includes('sign up')) { contentPieces.add(text); totalLength += text.length; } } }); // If still not enough, get the full text content if (totalLength < 300) { const fullText = articleEl.textContent?.trim(); if (fullText && fullText.length > 100 && !contentPieces.has(fullText)) { // Clean up the text const cleanText = fullText.replace(/\s+/g, ' ').substring(0, 1000); contentPieces.add(cleanText); totalLength += cleanText.length; } } }); } catch (e) { log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Article selector failed: ${selector}`); } } // If we don't have enough content, broaden the search if (totalLength < 500) { const additionalSelectors = [ 'main p', '[role="main"] p', '.content p', '.article-body p', '.story-body p', 'article p', '.post-content p', '.entry-content p' ]; additionalSelectors.forEach(selector => { if (totalLength >= MAX_CONTENT_LENGTH) return; try { const paragraphs = document.querySelectorAll(selector); paragraphs.forEach(p => { const text = p.textContent?.trim(); if (text && text.length > 30 && !contentPieces.has(text) && totalLength < MAX_CONTENT_LENGTH) { contentPieces.add(text); totalLength += text.length; } }); } catch (e) { log(LOG_LEVELS.VERBOSE, 'EXTRACT', `Additional selector failed: ${selector}`); } }); } // Convert Set to array and join data.content = Array.from(contentPieces).join('\n\n'); // If still insufficient, use fallback extraction if (data.content.length < 200) { log(LOG_LEVELS.INFO, 'EXTRACT', 'Insufficient content, using fallback extraction'); data.content = getPageTextContent(); } // Ensure we don't exceed reasonable limits if (data.content.length > MAX_CONTENT_LENGTH) { data.content = data.content.substring(0, MAX_CONTENT_LENGTH) + '...'; } log(LOG_LEVELS.INFO, 'EXTRACT', `Content extraction complete`, { length: data.content.length, pieces: contentPieces.size }); // Fallback to meta tags if (!data.title) { const metaTitle = document.querySelector('meta[property="og:title"], meta[name="twitter:title"]'); if (metaTitle) { data.title = metaTitle.getAttribute('content'); log(LOG_LEVELS.DEBUG, 'EXTRACT', 'Title extracted from meta tags'); } } if (!data.summary) { const metaDesc = document.querySelector('meta[property="og:description"], meta[name="description"]'); if (metaDesc) { data.summary = metaDesc.getAttribute('content'); log(LOG_LEVELS.DEBUG, 'EXTRACT', 'Summary extracted from meta tags'); } } log(LOG_LEVELS.INFO, 'EXTRACT', 'Article extraction complete', { hasTitle: !!data.title, hasAuthor: !!data.author, hasDate: !!data.date, hasSummary: !!data.summary, hasContent: !!data.content, contentLength: data.content?.length || 0 }); return data;}Readable shipped source: runtime message fields
content.js function triggerAIGeneration() { log(LOG_LEVELS.INFO, 'AI', '๐ค Auto-triggering AI poll generation'); chrome.runtime.sendMessage({ action: 'generatePollWithAI', data: { title: polypollContentState.articleData?.title || '', content: polypollContentState.articleData?.content || polypollContentState.articleData?.summary || '', url: polypollContentState.articleData?.url || window.location.href } }, (response) => { if (response && response.success) { log(LOG_LEVELS.INFO, 'AI', 'โ
AI generation successful', response.data); // Hide context menu loading if it exists hideContextMenuLoading(); // Hide loading skeletons and populate fields const questionSkeleton = modal.querySelector('#question-skeleton'); const aiLoadingMessage = modal.querySelector('#ai-loading-message'); const questionDisplay = modal.querySelector('#poll-question-display'); const questionText = modal.querySelector('#poll-question-text'); const questionInput = modal.querySelector('#poll-question'); const aiContainer = modal.querySelector('.ai-options-container'); const aiOptionsDiv = modal.querySelector('#ai-generated-options'); // Hide loading elements questionSkeleton.style.display = 'none'; questionSkeleton.classList.remove('show'); aiLoadingMessage.style.display = 'none'; // Populate question in the styled display const generatedQuestion = response.data.question || 'What will happen?'; questionText.innerHTML = generatedQuestion; questionText.classList.add('loaded'); questionInput.value = generatedQuestion; // Store in hidden input for form submission // Show AI options container and populate aiContainer.style.display = 'block'; // Clear existing AI options aiOptionsDiv.innerHTML = ''; // Add AI-generated options with proper emojis (randomized order) const perspectiveMap = { 'serial_optimism': '๐', 'contrarian': '๐', 'pessimistic': '๐', 'hottake': '๐ฅ' }; // Collect all valid options and randomize order const options = []; Object.entries(perspectiveMap).forEach(([key, emoji]) => { if (response.data[key]) { options.push({ key: key, emoji: emoji, text: response.data[key] }); } }); // Randomize the array options.sort(() => Math.random() - 0.5); // Render options in randomized order const stakeControls = modal.querySelector('#stake-controls'); options.forEach((option, index) => { const optionDiv = document.createElement('div'); optionDiv.className = 'perspective-option ai-generated clean-style'; optionDiv.dataset.optionId = `option-${index}`; optionDiv.innerHTML = ` <div class="option-indicator">${String.fromCharCode(65 + index)}</div> <div class="option-content"> <div class="option-emoji">${option.emoji}</div> <div class="option-text-wrapper"> <div class="poll-option ai-option" data-perspective="${option.key}"> ${option.text} </div> </div> </div> <div class="option-hover-effect"></div> `; // Add click handler for option selection optionDiv.addEventListener('click', () => { // Remove selected class from all options modal.querySelectorAll('.perspective-option').forEach(opt => { opt.classList.remove('selected'); }); // Add selected class to this option optionDiv.classList.add('selected'); // Show stake controls with animation if (stakeControls) { stakeControls.style.display = 'block'; // Scroll to stake controls smoothly setTimeout(() => { stakeControls.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 100); } }); aiOptionsDiv.appendChild(optionDiv); }); } else { log(LOG_LEVELS.ERROR, 'AI', 'โ AI generation failed', response?.error); // Hide context menu loading if it exists hideContextMenuLoading(); // Hide loading and show error state const questionSkeleton = modal.querySelector('#question-skeleton'); const aiLoadingMessage = modal.querySelector('#ai-loading-message'); const questionText = modal.querySelector('#poll-question-text'); const questionInput = modal.querySelector('#poll-question'); questionSkeleton.style.display = 'none'; aiLoadingMessage.innerHTML = ` <div class="ai-error-content"> <div class="error-icon">โ</div> <h3>AI Generation Failed</h3> <p>Please refresh and try again</p> </div> `; questionText.textContent = 'Failed to generate question - Please try again'; questionText.classList.add('error'); questionInput.value = ''; showNotification('Failed to generate poll with AI. Please create manually.', 'error'); } }); }Readable shipped source: backend POST branch
background.jsasync function handleAIPollGeneration(articleData) { log(LOG_LEVELS.DEBUG, 'AI', '๐ค Generating poll with new AI endpoint', { title: articleData?.title?.substring(0, 50), contentLength: articleData?.content?.length }); try { // Check if this is a Twitter/X URL const isTwitter = articleData.url && ( articleData.url.includes('twitter.com') || articleData.url.includes('x.com') ); let endpoint, requestBody; const apiBaseUrl = getApiBaseUrl(); if (isTwitter) { // Use Twitter instant suggest endpoint (ultra-fast) log(LOG_LEVELS.INFO, 'AI', '๐ฆ Detected Twitter/X content - using instant suggest'); endpoint = `${apiBaseUrl}/api/v1/twitter-instant-suggest`; requestBody = { tweet_content: articleData.content || articleData.summary || '', tweet_author: articleData.author || 'Unknown', tweet_url: articleData.url || null, user_id: 'twitter_extension' }; } else { // Use regular article poll endpoint endpoint = `${apiBaseUrl}/api/ai/generate-poll`; requestBody = { title: articleData.title || 'Untitled Article', content: articleData.content || articleData.summary || 'No content available', url: articleData.url || 'https://example.com' }; } log(LOG_LEVELS.DEBUG, 'AI', '๐ก Making request to AI endpoint', { endpoint: endpoint, bodyKeys: Object.keys(requestBody), isTwitter: isTwitter }); const response = await fetch(endpoint, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); log(LOG_LEVELS.ERROR, 'AI', `โ API request failed: ${response.status}`, errorText); throw new Error(`AI API error (${response.status}): ${errorText}`); } const aiResponse = await response.json(); log(LOG_LEVELS.INFO, 'AI', 'โ
AI response received', { hasQuestion: !!aiResponse.question, hasOptions: !!aiResponse.options, optionCount: aiResponse.options?.length || 0, isTwitterPoll: isTwitter }); // Handle Twitter binary poll response format if (isTwitter && aiResponse.options) { // Convert binary poll format to standard format if needed return { question: aiResponse.question, options: aiResponse.options.map(opt => ({ text: opt.text, reasoning: opt.reasoning, expected_percentage: opt.expected_percentage })), metadata: { poll_type: 'binary', confidence_level: aiResponse.confidence_level, resolution_timeframe: aiResponse.resolution_timeframe, ...aiResponse.metadata }, analysis: aiResponse.reasoning || aiResponse.analysis }; } // Return the full AI response with all the rich data for regular polls return { question: aiResponse.question, serial_optimism: aiResponse.serial_optimism, contrarian: aiResponse.contrarian, pessimistic: aiResponse.pessimistic, hottake: aiResponse.hottake, options: aiResponse.options, metadata: aiResponse.metadata, analysis: aiResponse.analysis }; } catch (error) { log(LOG_LEVELS.ERROR, 'AI', 'โ AI poll generation failed', error); throw error; }}- polypoll-backend-frg4.onrender.com
PolyPoll backend endpoint that receives article fields for AI poll generation.