// ============================================================================ // TINDER PLATFORM ADAPTER // ============================================================================ // // Tinder uses a REST API at api.gotinder.com. // Auth: x-auth-token header (extracted from browser session). // // Write capability is feasible — POST to /user/matches/{id} with the // x-auth-token is indistinguishable from the user sending a message. // // Verified: 2026-06-19 via HAR capture on tinder.com // ============================================================================ import type { PlatformAdapter, ApiEndpoint, Conversation, Message, Participant, } from "../index.js"; // ── Raw API Types ───────────────────────────────────────────────────────── export interface TinderMatchesResponse { data: { matches: TinderRawMatch[]; }; } export interface TinderRawMatch { _id: string; /** Whether there are unread messages */ seen: { match_seen: boolean }; /** The other person */ person: { _id: string; name: string; bio?: string; photos: Array<{ url: string; processedFiles: Array<{ url: string; width: number; height: number }>; }>; }; messages: TinderRawMessage[]; /** ISO date of last activity */ last_activity_date: string; /** Message count */ message_count: number; } export interface TinderRawMessage { _id: string; match_id: string; to: string; from: string; message: string; sent_date: string; // ISO date timestamp: number; // epoch ms } // ── Normalization ───────────────────────────────────────────────────────── export function normalizeConversation( match: TinderRawMatch, ): Conversation { const photo = match.person.photos[0]?.processedFiles?.find( (f) => f.width <= 172, ) ?? match.person.photos[0]?.processedFiles?.[0]; const participant: Participant = { id: match.person._id, name: match.person.name, avatarUrl: photo?.url ?? match.person.photos[0]?.url, }; const lastMsg = match.messages[0]; const lastMessage: Message | null = lastMsg ? { id: lastMsg._id, senderId: lastMsg.from, text: lastMsg.message, timestamp: lastMsg.sent_date, type: "text", } : null; return { platform: "tinder", conversationId: match._id, conversationType: "dm", name: match.person.name, participants: [participant], lastMessage, lastActivityAt: match.last_activity_date, unreadCount: match.seen?.match_seen ? 0 : 1, }; } export function normalizeMessage(msg: TinderRawMessage): Message { return { id: msg._id, senderId: msg.from, text: msg.message, timestamp: msg.sent_date, type: "text", }; } // ── Adapter Definition ──────────────────────────────────────────────────── const listConversations: ApiEndpoint = { name: "matches (v2)", method: "GET", urlTemplate: "https://api.gotinder.com/v2/matches?locale=en&count=60&message=1&is_tinder_u=false", headers: { "x-auth-token": "{authToken}", platform: "web", }, description: "Fetch matches with messages. count=60 is max per page. " + "message=1 filters to only matches with messages.", rateLimiting: "Medium risk. Tinder rate limits are moderate. Don't exceed " + "one call per 30 seconds.", verification: { status: "live_verified", lastChecked: "2026-06-19", source: "hydra_runtime", notes: "Hydra extension has used this REST path for matches and inline messages.", }, }; const fetchMessages: ApiEndpoint = { name: "match messages", method: "GET", urlTemplate: "https://api.gotinder.com/v2/matches/{matchId}/messages?locale=en&count=100", headers: { "x-auth-token": "{authToken}", platform: "web", }, description: "Fetch messages for a specific match. count=100 is max per page.", rateLimiting: "Space 1-3 seconds apart. Max 10 threads per sync.", verification: { status: "documented", lastChecked: "2026-06-19", source: "manual_capture", notes: "Documented REST path; Tinder match list often already includes recent messages when message=1.", }, }; const sendMessage: ApiEndpoint = { name: "send message", method: "POST", urlTemplate: "https://api.gotinder.com/user/matches/{matchId}", headers: { "x-auth-token": "{authToken}", "content-type": "application/json", platform: "web", }, bodyTemplate: { message: "{messageText}", }, description: "Send a message to a match. The x-auth-token from the browser session " + "makes this indistinguishable from the user sending a message.", rateLimiting: "Use sparingly. One message at a time. Draft/approval pattern recommended.", verification: { status: "documented", lastChecked: "2026-06-19", source: "manual_capture", notes: "Known feasible from web API shape; keep behind explicit approval before product use.", }, }; export const tinderAdapter: PlatformAdapter = { platform: "tinder", name: "Tinder", color: "#FF6B6B", emoji: "🔥", auth: { requiredTokens: ["authToken"], extractionMethod: "Content script on *.tinder.com extracts the x-auth-token from " + "XHR requests in the page context, or from localStorage.", sources: [ { type: "local_storage", name: "authToken", selector: "TinderWeb/APIToken", description: "The x-auth-token. Found in localStorage under TinderWeb/APIToken " + "or captured from outgoing XHR requests.", }, ], }, endpoints: { listConversations, fetchMessages, sendMessage, }, antiDetection: { minDelayMs: 1000, maxDelayMs: 3000, maxMessageThreads: 10, notes: "Tinder's detection is moderate. The main risk is volume — " + "fetching too many matches too fast. The write API (sendMessage) " + "should only be used through a draft/approval pattern.", }, buildDeepLink: (conv) => { return `https://tinder.com/app/messages/${conv.conversationId}`; }, lastVerified: "2026-06-19", changelog: [ { date: "2026-06-19", description: "Initial adapter. REST API verified for matches + messages + send.", }, ], };