spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: server/services/serpapiService.ts6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { getJson } from 'serpapi';1819const SERPAPI_API_KEY = process.env.SERPAPI_API_KEY;2021export interface ImageResult {22 title: string;23 url: string;24 thumbnail: string;25 source: string;26 width?: number;27 height?: number;28}2930export interface MapDirections {31 from: string;32 to: string;33 distance: string;34 duration: string;35 steps: Array<{36 instruction: string;37 distance: string;38 duration: string;39 }>;40 mapUrl: string;41}4243export interface ShoppingProduct {44 title: string;45 price: string;46 source: string;47 link: string;48 thumbnail: string;49 rating?: number;50 reviews?: number;51 delivery?: string;52}5354export async function searchImages(query: string, limit: number = 10): Promise<ImageResult[]> {55 if (!SERPAPI_API_KEY) {56 throw new Error('SERPAPI_API_KEY not configured');57 }5859 try {60 const response = await getJson({61 engine: 'google_images',62 q: query,63 api_key: SERPAPI_API_KEY,64 num: Math.min(limit, 20),65 });6667 const images: ImageResult[] = (response.images_results || []).slice(0, limit).map((img: any) => ({68 title: img.title || '',69 url: img.original || img.link || '',70 thumbnail: img.thumbnail || img.original || '',71 source: img.source || '',72 width: img.original_width,73 height: img.original_height,74 }));7576 return images;77 } catch (error) {78 console.error('SerpAPI image search error:', error);79 throw new Error(`Failed to search images: ${(error as Error).message}`);80 }81}8283export async function getMapDirections(from: string, to: string): Promise<MapDirections> {84 if (!SERPAPI_API_KEY) {85 throw new Error('SERPAPI_API_KEY not configured');86 }8788 try {89 const response = await getJson({90 engine: 'google_maps_directions',91 start_addr: from,92 end_addr: to,93 api_key: SERPAPI_API_KEY,94 });9596 const directions = response.directions || [];97 const route = directions[0] || {};98 const placesInfo = response.places_info || [];99 100 const steps = (route.steps || []).map((step: any) => ({101 instruction: step.instruction || '',102 distance: formatDistance(step.distance),103 duration: step.duration || '',104 }));105106 const mapUrl = `https://www.google.com/maps/dir/?api=1&origin=${encodeURIComponent(from)}&destination=${encodeURIComponent(to)}`;107108 return {109 from: placesInfo[0]?.address || from,110 to: placesInfo[1]?.address || to,111 distance: formatDistance(route.distance),112 duration: route.duration || 'N/A',113 steps: steps.slice(0, 10),114 mapUrl,115 };116 } catch (error) {117 console.error('SerpAPI directions error:', error);118 throw new Error(`Failed to get directions: ${(error as Error).message}`);119 }120}121122function formatDistance(meters: number | undefined): string {123 if (!meters) return 'N/A';124 if (meters >= 1000) {125 return `${(meters / 1000).toFixed(1)} km`;126 }127 return `${meters} m`;128}129130export async function searchGoogleShopping(query: string, limit: number = 10): Promise<ShoppingProduct[]> {131 if (!SERPAPI_API_KEY) {132 throw new Error('SERPAPI_API_KEY not configured');133 }134135 try {136 const response = await getJson({137 engine: 'google_shopping',138 q: query,139 api_key: SERPAPI_API_KEY,140 num: Math.min(limit, 20),141 });142143 const products: ShoppingProduct[] = (response.shopping_results || []).slice(0, limit).map((product: any) => ({144 title: product.title || '',145 price: product.price || product.extracted_price || 'N/A',146 source: product.source || '',147 link: product.link || '',148 thumbnail: product.thumbnail || '',149 rating: product.rating ? parseFloat(product.rating) : undefined,150 reviews: product.reviews ? parseInt(product.reviews) : undefined,151 delivery: product.delivery || undefined,152 }));153154 return products;155 } catch (error) {156 console.error('SerpAPI shopping search error:', error);157 throw new Error(`Failed to search products: ${(error as Error).message}`);158 }159}160161export interface FinanceData {162 symbol: string;163 price: string;164 currency: string;165 changePercent?: string;166 marketCap?: string;167 priceHistory?: Array<{168 date: string;169 price: number;170 }>;171}172173export async function searchFinance(query: string): Promise<FinanceData> {174 if (!SERPAPI_API_KEY) {175 throw new Error('SERPAPI_API_KEY not configured');176 }177178 try {179 const response = await getJson({180 engine: 'google_finance',181 q: query,182 api_key: SERPAPI_API_KEY,183 });184185 const summary = response.summary || {};186 const graph = response.graph || [];187 188 const extractedPrice = summary.extracted_price;189 const price = extractedPrice ? `${extractedPrice}` : (summary.price || 'N/A');190 const currency = summary.currency || 'USD';191 const symbol = summary.stock || query;192 193 let changePercent = summary.price_movement?.percentage;194 if (changePercent) {195 changePercent = changePercent > 0 ? `+${changePercent.toFixed(2)}%` : `${changePercent.toFixed(2)}%`;196 }197 198 const priceHistory = graph.slice(-30).map((point: any) => ({199 date: point.date || '',200 price: parseFloat(point.price || 0),201 })).filter((p: any) => p.price > 0 && p.date);202203 return {204 symbol,205 price,206 currency,207 changePercent,208 marketCap: summary.market_cap || undefined,209 priceHistory: priceHistory.length > 0 ? priceHistory : undefined,210 };211 } catch (error) {212 console.error('SerpAPI finance search error:', error);213 throw new Error(`Failed to search finance data: ${(error as Error).message}`);214 }215}216217export interface JobPosting {218 title: string;219 company: string;220 location: string;221 description: string;222 link: string;223 thumbnail?: string;224 postedAt?: string;225 salary?: string;226}227228export async function searchJobs(query: string, location?: string, limit: number = 10): Promise<JobPosting[]> {229 if (!SERPAPI_API_KEY) {230 throw new Error('SERPAPI_API_KEY not configured');231 }232233 try {234 const response = await getJson({235 engine: 'google_jobs',236 q: query,237 location: location || '',238 api_key: SERPAPI_API_KEY,239 });240241 const jobs: JobPosting[] = (response.jobs_results || []).slice(0, limit).map((job: any) => ({242 title: job.title || '',243 company: job.company_name || '',244 location: job.location || '',245 description: job.description || '',246 link: job.share_link || job.apply_link || '',247 thumbnail: job.thumbnail || undefined,248 postedAt: job.detected_extensions?.posted_at || undefined,249 salary: job.detected_extensions?.salary || undefined,250 }));251252 return jobs;253 } catch (error) {254 console.error('SerpAPI jobs search error:', error);255 throw new Error(`Failed to search jobs: ${(error as Error).message}`);256 }257}258259export interface FlightOption {260 airline: string;261 flightNumber?: string;262 departure: {263 airport: string;264 time: string;265 };266 arrival: {267 airport: string;268 time: string;269 };270 duration: string;271 price: string;272 stops?: number;273 link?: string;274}275276export async function searchFlights(277 departureId: string,278 arrivalId: string,279 outboundDate: string,280 returnDate?: string281): Promise<FlightOption[]> {282 if (!SERPAPI_API_KEY) {283 throw new Error('SERPAPI_API_KEY not configured');284 }285286 try {287 const response = await getJson({288 engine: 'google_flights',289 departure_id: departureId,290 arrival_id: arrivalId,291 outbound_date: outboundDate,292 return_date: returnDate,293 api_key: SERPAPI_API_KEY,294 currency: 'USD',295 });296297 const flights: FlightOption[] = (response.best_flights || response.other_flights || []).slice(0, 10).map((flight: any) => {298 const firstFlight = flight.flights?.[0] || {};299 return {300 airline: firstFlight.airline || 'Unknown',301 flightNumber: firstFlight.flight_number || undefined,302 departure: {303 airport: firstFlight.departure_airport?.id || departureId,304 time: firstFlight.departure_airport?.time || '',305 },306 arrival: {307 airport: firstFlight.arrival_airport?.id || arrivalId,308 time: firstFlight.arrival_airport?.time || '',309 },310 duration: flight.total_duration || 'N/A',311 price: flight.price || 'N/A',312 stops: (flight.flights?.length || 1) - 1,313 link: flight.booking_token ? `https://www.google.com/travel/flights` : undefined,314 };315 });316317 return flights;318 } catch (error) {319 console.error('SerpAPI flights search error:', error);320 throw new Error(`Failed to search flights: ${(error as Error).message}`);321 }322}323324export interface HotelOption {325 name: string;326 rating?: number;327 reviews?: number;328 price: string;329 thumbnail?: string;330 link: string;331 address?: string;332 amenities?: string[];333}334335export async function searchHotels(336 query: string,337 checkInDate?: string,338 checkOutDate?: string,339 limit: number = 10340): Promise<HotelOption[]> {341 if (!SERPAPI_API_KEY) {342 throw new Error('SERPAPI_API_KEY not configured');343 }344345 if (checkInDate && !checkOutDate) {346 throw new Error('check_out_date is required when check_in_date is provided');347 }348349 try {350 const params: any = {351 engine: 'google_hotels',352 q: query,353 api_key: SERPAPI_API_KEY,354 currency: 'USD',355 };356357 if (checkInDate && checkOutDate) {358 params.check_in_date = checkInDate;359 params.check_out_date = checkOutDate;360 }361362 const response = await getJson(params);363364 const hotels: HotelOption[] = (response.properties || []).slice(0, limit).map((hotel: any) => ({365 name: hotel.name || '',366 rating: hotel.overall_rating ? parseFloat(hotel.overall_rating) : undefined,367 reviews: hotel.reviews ? parseInt(hotel.reviews) : undefined,368 price: hotel.rate_per_night?.lowest || hotel.total_rate?.lowest || 'N/A',369 thumbnail: hotel.images?.[0]?.thumbnail || undefined,370 link: hotel.link || '',371 address: hotel.description || undefined,372 amenities: hotel.amenities?.slice(0, 5) || undefined,373 }));374375 return hotels;376 } catch (error) {377 console.error('SerpAPI hotels search error:', error);378 throw new Error(`Failed to search hotels: ${(error as Error).message}`);379 }380}381382export interface VideoResult {383 title: string;384 link: string;385 thumbnail: string;386 channel: string;387 duration?: string;388 publishedDate?: string;389 views?: string;390}391392export async function searchVideos(query: string, limit: number = 10): Promise<VideoResult[]> {393 if (!SERPAPI_API_KEY) {394 throw new Error('SERPAPI_API_KEY not configured');395 }396397 try {398 const response = await getJson({399 engine: 'google_videos',400 q: query,401 api_key: SERPAPI_API_KEY,402 });403404 const videos: VideoResult[] = (response.video_results || []).slice(0, limit).map((video: any) => ({405 title: video.title || '',406 link: video.link || '',407 thumbnail: video.thumbnail || '',408 channel: video.channel || video.source || '',409 duration: video.duration || undefined,410 publishedDate: video.published_date || video.date || undefined,411 views: video.views || undefined,412 }));413414 return videos;415 } catch (error) {416 console.error('SerpAPI videos search error:', error);417 throw new Error(`Failed to search videos: ${(error as Error).message}`);418 }419}420421export interface GoogleTrendData {422 query: string;423 interest: Array<{424 date: string;425 value: number;426 }>;427 relatedQueries?: Array<{428 query: string;429 value: number;430 }>;431 risingQueries?: Array<{432 query: string;433 value: string;434 }>;435}436437export async function searchGoogleTrends(query: string): Promise<GoogleTrendData> {438 if (!SERPAPI_API_KEY) {439 throw new Error('SERPAPI_API_KEY not configured');440 }441442 try {443 const response = await getJson({444 engine: 'google_trends',445 q: query,446 api_key: SERPAPI_API_KEY,447 data_type: 'TIMESERIES',448 });449450 const interestOverTime = response.interest_over_time?.timeline_data || [];451 const relatedQueries = response.related_queries?.top || [];452 const risingQueries = response.related_queries?.rising || [];453454 return {455 query,456 interest: interestOverTime.slice(0, 30).map((point: any) => ({457 date: point.date || '',458 value: point.values?.[0]?.extracted_value || 0,459 })),460 relatedQueries: relatedQueries.slice(0, 10).map((q: any) => ({461 query: q.query || '',462 value: q.value || 0,463 })),464 risingQueries: risingQueries.slice(0, 10).map((q: any) => ({465 query: q.query || '',466 value: q.value || 'N/A',467 })),468 };469 } catch (error) {470 console.error('SerpAPI Google Trends search error:', error);471 throw new Error(`Failed to search Google Trends: ${(error as Error).message}`);472 }473}474475export interface ScholarArticle {476 title: string;477 link: string;478 authors?: string;479 publication: string;480 year?: string;481 citedBy?: number;482 snippet: string;483 pdfLink?: string;484}485486export async function searchGoogleScholar(query: string, limit: number = 10): Promise<ScholarArticle[]> {487 if (!SERPAPI_API_KEY) {488 throw new Error('SERPAPI_API_KEY not configured');489 }490491 try {492 const response = await getJson({493 engine: 'google_scholar',494 q: query,495 api_key: SERPAPI_API_KEY,496 num: Math.min(limit, 20),497 });498499 const articles: ScholarArticle[] = (response.organic_results || []).slice(0, limit).map((article: any) => ({500 title: article.title || '',501 link: article.link || '',502 authors: article.publication_info?.authors?.map((a: any) => a.name).join(', ') || undefined,503 publication: article.publication_info?.summary || '',504 year: article.publication_info?.summary?.match(/\d{4}/)?.[0] || undefined,505 citedBy: article.inline_links?.cited_by?.total ? parseInt(article.inline_links.cited_by.total) : undefined,506 snippet: article.snippet || '',507 pdfLink: article.resources?.find((r: any) => r.file_format === 'PDF')?.link || undefined,508 }));509510 return articles;511 } catch (error) {512 console.error('SerpAPI Google Scholar search error:', error);513 throw new Error(`Failed to search Google Scholar: ${(error as Error).message}`);514 }515}516517export interface AppStoreApp {518 title: string;519 link: string;520 appId: string;521 developer: string;522 rating?: number;523 ratingCount?: number;524 price: string;525 thumbnail?: string;526 description: string;527 category?: string;528}529530export async function searchAppStore(query: string, limit: number = 10): Promise<AppStoreApp[]> {531 if (!SERPAPI_API_KEY) {532 throw new Error('SERPAPI_API_KEY not configured');533 }534535 try {536 const response = await getJson({537 engine: 'apple_app_store',538 term: query,539 api_key: SERPAPI_API_KEY,540 num: Math.min(limit, 20),541 });542543 const apps: AppStoreApp[] = (response.organic_results || []).slice(0, limit).map((app: any) => ({544 title: app.title || '',545 link: app.link || '',546 appId: app.product_id || '',547 developer: app.developer || '',548 rating: app.rating ? parseFloat(app.rating) : undefined,549 ratingCount: app.reviews ? parseInt(app.reviews) : undefined,550 price: app.price || 'Free',551 thumbnail: app.thumbnail || undefined,552 description: app.description || '',553 category: app.category || undefined,554 }));555556 return apps;557 } catch (error) {558 console.error('SerpAPI App Store search error:', error);559 throw new Error(`Failed to search App Store: ${(error as Error).message}`);560 }561}562