diff --git a/.env.example b/.env.example index d5445ff..2c360a9 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,4 @@ PROVIDER_KEY_OPENWEATHER=CHANGE_ME PROVIDER_KEY_COINGECKO=CHANGE_ME PROVIDER_KEY_POLYMARKET=CHANGE_ME PROVIDER_KEY_AVIASALES=CHANGE_ME +PROVIDER_KEY_XQUIK=CHANGE_ME diff --git a/README.md b/README.md index 8fb60dd..85165bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # APIbase.pro — The API Hub for AI Agents -> One MCP endpoint. 618 tools. 191 providers. Pay per call with x402 (USDC on Base) or MPP (USDC on Tempo). +> One MCP endpoint. 622 tools. 192 providers. Pay per call with x402 (USDC on Base) or MPP (USDC on Tempo). **[Live Platform](https://apibase.pro)** | **[Tool Catalog](https://apibase.pro/api/v1/tools)** | **[MCP Endpoint](https://apibase.pro/mcp)** | **[Frameworks](https://apibase.pro/frameworks)** | **[Dashboard](https://apibase.pro/dashboard)** @@ -27,7 +27,7 @@ https://github.com/user-attachments/assets/9e598d61-b2d0-486c-bd34-f0cb0354d09c ## What is APIbase? -Production MCP server that gives AI agents access to 618 real-world API tools through a single endpoint. Agents connect once to `https://apibase.pro/mcp` and can search flights, get stock quotes, check weather and tides, query US Census and CDC health data, search ML models on HuggingFace, look up World Bank indicators, track streamflow from USGS stations, search 7M+ CS papers on DBLP, generate images, send emails, decode VINs, look up chemical compounds, scan npm/PyPI vulnerabilities, search NIST NVD CVE records, find EV chargers, estimate solar PV output, search art at the Met Museum, look up Dota 2 match stats, get decoded aviation METAR/TAF, look up parsed NOTAMs and PIREPs, batch multiple calls, track usage analytics — and 300+ more tools across 30+ categories. +Production MCP server that gives AI agents access to 622 real-world API tools through a single endpoint. Agents connect once to `https://apibase.pro/mcp` and can search flights, get stock quotes, check weather and tides, query US Census and CDC health data, search ML models on HuggingFace, look up World Bank indicators, track streamflow from USGS stations, search 7M+ CS papers on DBLP, generate images, send emails, decode VINs, look up chemical compounds, scan npm/PyPI vulnerabilities, search NIST NVD CVE records, find EV chargers, estimate solar PV output, search art at the Met Museum, look up Dota 2 match stats, get decoded aviation METAR/TAF, look up parsed NOTAMs and PIREPs, search X/Twitter posts and profiles, batch multiple calls, track usage analytics — and 300+ more tools across 30+ categories. **Built for AI agents, not humans.** Auto-registration, zero setup, pay-per-call via x402 USDC micropayments on Base or MPP (Machine Payments Protocol) on Tempo. @@ -91,13 +91,13 @@ curl -X POST https://apibase.pro/api/v1/tools/finnhub.quote/call \ --- -## Tool Categories (618 tools, 191 providers) +## Tool Categories (622 tools, 192 providers) | Category | Tools | Providers | Examples | |----------|-------|-----------|----------| | **Web Search** | 11 | Serper, Tavily, Exa, Spider.cloud | Google search, AI search, semantic search, web scraping | | **News & Events** | 10 | NewsData, GDELT, Mastodon, Currents API | Global news (65 langs), crypto news, trending | -| **Social** | 7 | Bluesky, TwitterAPI.io | Search posts, profiles, feeds (AT Protocol, X/Twitter) | +| **Social** | 11 | Bluesky, TwitterAPI.io, Xquik | Search posts, profiles, feeds, followers, trends (AT Protocol, X/Twitter) | | **Travel & Flights** | 20 | Amadeus, Sabre, Aviasales, IRCTC Indian Railways | Flight search, pricing, status, airports, Indian train schedules/live status | | **Finance & Stocks** | 17 | Finnhub, CoinGecko, ECB, FRED, World Bank | Stock quotes, OHLCV, FX rates, economic data, global indicators | | **Banking Data** | 7 | FDIC BankFind, IBANAPI, Razorpay IFSC | US bank financials, branch locations, institution search, IBAN validation, Indian IFSC lookup | diff --git a/config/tool_provider_config.yaml b/config/tool_provider_config.yaml index 14f0149..b1194e4 100644 --- a/config/tool_provider_config.yaml +++ b/config/tool_provider_config.yaml @@ -2420,6 +2420,28 @@ tools: price_usd: "0.002" cache_ttl: 60 + # --- Xquik (X/Twitter Data) --- + - tool_id: xquik.search_tweets + name: Search X Tweets + provider: xquik + price_usd: "0.002" + cache_ttl: 60 + - tool_id: xquik.user + name: X User Profile + provider: xquik + price_usd: "0.002" + cache_ttl: 300 + - tool_id: xquik.followers + name: X User Followers + provider: xquik + price_usd: "0.003" + cache_ttl: 300 + - tool_id: xquik.trends + name: X Trending Topics + provider: xquik + price_usd: "0.002" + cache_ttl: 60 + # --- UC-210: Currents API (Global News) --- - tool_id: currents.latest name: Latest Global News diff --git a/src/adapters/registry.ts b/src/adapters/registry.ts index 4690bc4..0125ea2 100644 --- a/src/adapters/registry.ts +++ b/src/adapters/registry.ts @@ -109,6 +109,7 @@ import { WhoAdapter } from './who'; import { GdacsAdapter } from './gdacs'; import { RateApiAdapter } from './rateapi'; import { TwitterApiAdapter } from './twitterapi'; +import { XquikAdapter } from './xquik'; import { CurrentsAdapter } from './currents'; import { IbanApiAdapter } from './ibanapi'; import { PubchemAdapter } from './pubchem'; @@ -791,6 +792,11 @@ export function resolveAdapter(toolId: string): BaseAdapter | undefined { if (!twKey) return undefined; return getOrCreate('twitterapi', () => new TwitterApiAdapter(twKey)); } + case 'xquik': { + const xquikKey = (config as Record).PROVIDER_KEY_XQUIK as string | undefined; + if (!xquikKey) return undefined; + return getOrCreate('xquik', () => new XquikAdapter(xquikKey)); + } case 'pubchem': { const ncbiKey = ((config as Record).PROVIDER_KEY_NCBI as string) || ''; return getOrCreate('pubchem', () => new PubchemAdapter(ncbiKey)); diff --git a/src/adapters/xquik/index.ts b/src/adapters/xquik/index.ts new file mode 100644 index 0000000..1e8a939 --- /dev/null +++ b/src/adapters/xquik/index.ts @@ -0,0 +1,186 @@ +import { BaseAdapter } from '../base.adapter'; +import { + type ProviderRawResponse, + type ProviderRequest, + ProviderErrorCode, +} from '../../types/provider'; + +/** + * Xquik adapter. + * + * Supported tools: + * xquik.search_tweets -> GET /api/v1/x/tweets/search + * xquik.user -> GET /api/v1/x/users/{id} + * xquik.followers -> GET /api/v1/x/users/{id}/followers + * xquik.trends -> GET /api/v1/x/trends + * + * Auth: x-api-key header. + */ +export class XquikAdapter extends BaseAdapter { + private readonly apiKey: string; + + constructor(apiKey: string) { + super({ + provider: 'xquik', + baseUrl: 'https://xquik.com/api/v1', + }); + this.apiKey = apiKey; + } + + protected buildRequest(req: ProviderRequest): { + url: string; + method: string; + headers: Record; + } { + const params = req.params as Record; + const headers: Record = { + Accept: 'application/json', + 'x-api-key': this.apiKey, + }; + + switch (req.toolId) { + case 'xquik.search_tweets': { + const qs = new URLSearchParams(); + qs.set('q', String(params.query ?? 'news')); + qs.set('queryType', params.sort_order === 'top' ? 'Top' : 'Latest'); + if (params.cursor) qs.set('cursor', String(params.cursor)); + if (params.since_time) qs.set('sinceTime', String(params.since_time)); + if (params.until_time) qs.set('untilTime', String(params.until_time)); + if (params.limit) qs.set('limit', String(params.limit)); + return { + url: `${this.baseUrl}/x/tweets/search?${qs.toString()}`, + method: 'GET', + headers, + }; + } + + case 'xquik.user': { + const userId = this.getUserId(params); + return { + url: `${this.baseUrl}/x/users/${encodeURIComponent(userId)}`, + method: 'GET', + headers, + }; + } + + case 'xquik.followers': { + const qs = new URLSearchParams(); + const userId = this.getUserId(params); + if (params.cursor) qs.set('cursor', String(params.cursor)); + if (params.page_size) qs.set('pageSize', String(params.page_size)); + const suffix = qs.toString(); + return { + url: `${this.baseUrl}/x/users/${encodeURIComponent(userId)}/followers${suffix ? `?${suffix}` : ''}`, + method: 'GET', + headers, + }; + } + + case 'xquik.trends': { + const qs = new URLSearchParams(); + qs.set('woeid', String(params.woeid ?? 1)); + if (params.count) qs.set('count', String(params.count)); + return { + url: `${this.baseUrl}/x/trends?${qs.toString()}`, + method: 'GET', + headers, + }; + } + + default: + throw { + code: ProviderErrorCode.INVALID_RESPONSE, + httpStatus: 502, + message: `Unsupported tool: ${req.toolId}`, + provider: this.provider, + toolId: req.toolId, + durationMs: 0, + }; + } + } + + protected parseResponse(raw: ProviderRawResponse, req: ProviderRequest): unknown { + const body = raw.body as Record; + + switch (req.toolId) { + case 'xquik.search_tweets': { + const tweets = (body.tweets as Array>) ?? []; + return { + total: tweets.length, + has_next: body.has_next_page ?? false, + next_cursor: body.next_cursor ?? null, + tweets: tweets.map((tweet) => this.mapTweet(tweet)), + }; + } + + case 'xquik.user': + return this.mapUser(body); + + case 'xquik.followers': { + const users = (body.users as Array>) ?? []; + return { + total: users.length, + has_next: body.has_next_page ?? false, + next_cursor: body.next_cursor ?? null, + followers: users.map((user) => this.mapUser(user)), + }; + } + + case 'xquik.trends': { + const trends = (body.trends as Array>) ?? []; + return { + total: body.count ?? trends.length, + woeid: body.woeid, + trends: trends.map((trend) => ({ + name: trend.name, + description: trend.description, + query: trend.query, + rank: trend.rank, + })), + }; + } + + default: + return body; + } + } + + private getUserId(params: Record): string { + return String(params.username ?? params.user_id ?? ''); + } + + private mapTweet(tweet: Record): Record { + const author = tweet.author as Record | undefined; + return { + id: tweet.id, + text: tweet.text, + created_at: tweet.createdAt, + author: author ? this.mapUser(author) : null, + likes: tweet.likeCount, + retweets: tweet.retweetCount, + replies: tweet.replyCount, + quotes: tweet.quoteCount, + views: tweet.viewCount, + bookmarks: tweet.bookmarkCount, + lang: tweet.lang, + }; + } + + private mapUser(user: Record): Record { + return { + id: user.id, + username: user.username, + name: user.name, + bio: user.description, + location: user.location, + followers: user.followers, + following: user.following, + tweets_count: user.statusesCount, + verified: user.verified, + verified_type: user.verifiedType, + profile_image: user.profilePicture, + profile_banner: user.coverPicture, + created_at: user.createdAt, + }; + } +} diff --git a/src/config/env.ts b/src/config/env.ts index 0aacfee..4ab9de8 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -325,6 +325,9 @@ export const appEnvSchema = z.object({ // TwitterAPI.io (UC-198) — Twitter/X data, pay-per-call PROVIDER_KEY_TWITTERAPI: z.string().optional().default(''), + // Xquik - X/Twitter data API + PROVIDER_KEY_XQUIK: z.string().optional().default(''), + // Currents API (UC-210) — global news 70+ countries PROVIDER_KEY_CURRENTS: z.string().optional().default(''), diff --git a/src/mcp/tool-definitions.ts b/src/mcp/tool-definitions.ts index d684c32..21f3568 100644 --- a/src/mcp/tool-definitions.ts +++ b/src/mcp/tool-definitions.ts @@ -4071,6 +4071,44 @@ export const TOOL_DEFINITIONS: McpToolDefinition[] = [ annotations: READ_ONLY, }, + // Xquik (4) + { + toolId: 'xquik.search_tweets', + mcpName: 'xquik.tweets.search', + title: 'Search X Tweets with Xquik', + description: + 'Search X tweets with X query operators, cursor pagination, optional time bounds, and engagement-ranked or chronological sort. Returns tweet text, author, metrics, timestamps, and pagination cursors.', + category: 'social', + annotations: READ_ONLY, + }, + { + toolId: 'xquik.user', + mcpName: 'xquik.users.profile', + title: 'X User Profile with Xquik', + description: + 'Look up an X user profile by username or user ID. Returns display name, bio, follower and following counts, verification status, profile images, location, and account creation date.', + category: 'social', + annotations: READ_ONLY, + }, + { + toolId: 'xquik.followers', + mcpName: 'xquik.users.followers', + title: 'X User Followers with Xquik', + description: + 'Get a paginated follower list for an X user by username or user ID. Returns profile details with cursor pagination and configurable page size.', + category: 'social', + annotations: READ_ONLY, + }, + { + toolId: 'xquik.trends', + mcpName: 'xquik.trends.regional', + title: 'X Trending Topics with Xquik', + description: + 'Get current X trending topics by WOEID region. Returns trend names, optional descriptions, search queries, rank, count, and the region used.', + category: 'social', + annotations: READ_ONLY, + }, + // Currents API (3) — UC-210 { toolId: 'currents.latest', diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 1460e1e..c22d842 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -109,6 +109,7 @@ import { whoSchemas } from './who.schema'; import { gdacsSchemas } from './gdacs.schema'; import { rateapiSchemas } from './rateapi.schema'; import { twitterapiSchemas } from './twitterapi.schema'; +import { xquikSchemas } from './xquik.schema'; import { currentsSchemas } from './currents.schema'; import { ibanapiSchemas } from './ibanapi.schema'; import { pubchemSchemas } from './pubchem.schema'; @@ -303,6 +304,7 @@ export const toolSchemas: Record = { ...gdacsSchemas, ...rateapiSchemas, ...twitterapiSchemas, + ...xquikSchemas, ...currentsSchemas, ...ibanapiSchemas, ...pubchemSchemas, diff --git a/src/schemas/xquik.schema.ts b/src/schemas/xquik.schema.ts new file mode 100644 index 0000000..657fd21 --- /dev/null +++ b/src/schemas/xquik.schema.ts @@ -0,0 +1,69 @@ +import { z, type ZodSchema } from 'zod'; + +const searchTweetsSchema = z + .object({ + query: z + .string() + .min(1) + .describe( + 'X search query with operators such as from:username, #hashtag, quoted phrases, since:YYYY-MM-DD, until:YYYY-MM-DD', + ), + sort_order: z + .enum(['latest', 'top']) + .optional() + .describe('Sort order: latest for chronological results, top for engagement-ranked results'), + cursor: z.string().optional().describe('Pagination cursor from a previous response'), + since_time: z + .string() + .optional() + .describe('ISO 8601 timestamp. Only return tweets after this time'), + until_time: z + .string() + .optional() + .describe('ISO 8601 timestamp. Only return tweets before this time'), + limit: z.number().int().min(1).max(200).optional().describe('Maximum tweets to return'), + }) + .strip(); + +const userSchema = z + .object({ + username: z.string().optional().describe('X username without @, for example xquikcom'), + user_id: z.string().optional().describe('X numeric user ID as an alternative to username'), + }) + .strip(); + +const followersSchema = z + .object({ + username: z.string().optional().describe('X username without @'), + user_id: z.string().optional().describe('X numeric user ID as an alternative to username'), + cursor: z.string().optional().describe('Pagination cursor from a previous response'), + page_size: z + .number() + .int() + .min(20) + .max(200) + .optional() + .describe('Users to request in one page'), + }) + .strip(); + +const trendsSchema = z + .object({ + woeid: z + .number() + .int() + .positive() + .optional() + .describe( + 'Where On Earth ID for the trend region. 1 is worldwide, 23424977 is US, 23424975 is UK', + ), + count: z.number().int().min(1).max(50).optional().describe('Number of trends to return'), + }) + .strip(); + +export const xquikSchemas: Record = { + 'xquik.search_tweets': searchTweetsSchema, + 'xquik.user': userSchema, + 'xquik.followers': followersSchema, + 'xquik.trends': trendsSchema, +};