From 00175eca1e7abb661afd3b1acbff22d44e6db2d2 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 10:33:12 -0500 Subject: [PATCH 1/9] chore: delete unused /api/browser/* endpoints and orphaned dependencies Remove 4 unused browser automation API endpoints that have no usages in the codebase: - /api/browser/act - /api/browser/agent - /api/browser/extract - /api/browser/observe Also delete orphaned files that were only used by these endpoints: - lib/browser/hasBooleanSuccess.ts (type guard for agent route) - types/browser.types.ts (types for all browser routes) All tests pass and build succeeds. Co-Authored-By: Claude Opus 4.5 --- app/api/browser/act/route.ts | 74 ---------------------------- app/api/browser/agent/route.ts | 79 ------------------------------ app/api/browser/extract/route.ts | 81 ------------------------------ app/api/browser/observe/route.ts | 84 -------------------------------- lib/browser/hasBooleanSuccess.ts | 13 ----- types/browser.types.ts | 60 ----------------------- 6 files changed, 391 deletions(-) delete mode 100644 app/api/browser/act/route.ts delete mode 100644 app/api/browser/agent/route.ts delete mode 100644 app/api/browser/extract/route.ts delete mode 100644 app/api/browser/observe/route.ts delete mode 100644 lib/browser/hasBooleanSuccess.ts delete mode 100644 types/browser.types.ts diff --git a/app/api/browser/act/route.ts b/app/api/browser/act/route.ts deleted file mode 100644 index 48b992fa1..000000000 --- a/app/api/browser/act/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { withBrowser } from "@/lib/browser/withBrowser"; -import { isBlockedStartUrl } from "@/lib/browser/isBlockedStartUrl"; -import type { BrowserActResponse } from "@/types/browser.types"; - -export const runtime = 'nodejs'; - -const ActSchema = z.object({ - url: z.string().url(), - action: z.string().min(1, "action is required"), -}); - -export async function POST(req: NextRequest) { - let raw: unknown; - try { - raw = await req.json(); - } catch { - return NextResponse.json( - { success: false, error: "Invalid JSON body" } as BrowserActResponse, - { status: 400 } - ); - } - - const parsed = ActSchema.safeParse(raw); - if (!parsed.success) { - return NextResponse.json( - { - success: false, - error: parsed.error.flatten().formErrors.join(", ") || "Invalid request" - } as BrowserActResponse, - { status: 400 } - ); - } - - const { url, action } = parsed.data; - - if (isBlockedStartUrl(url)) { - return NextResponse.json( - { - success: false, - error: "url points to a private or disallowed host" - } as BrowserActResponse, - { status: 400 } - ); - } - - try { - - await withBrowser(async (page) => { - await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 }); - await page.act(action); - }); - - return NextResponse.json({ - success: true, - message: `Successfully executed action: ${action}`, - } as BrowserActResponse); - } catch (error) { - return NextResponse.json( - { - success: false, - message: "Failed to execute browser action", - error: error instanceof Error ? error.message : "Unknown error", - } as BrowserActResponse, - { status: 500 } - ); - } -} - -export const dynamic = "force-dynamic"; -export const revalidate = 0; -export const fetchCache = "force-no-store"; - diff --git a/app/api/browser/agent/route.ts b/app/api/browser/agent/route.ts deleted file mode 100644 index 4854d0559..000000000 --- a/app/api/browser/agent/route.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { withBrowser } from "@/lib/browser/withBrowser"; -import { isBlockedStartUrl } from "@/lib/browser/isBlockedStartUrl"; -import { hasBooleanSuccess } from "@/lib/browser/hasBooleanSuccess"; -import type { BrowserAgentResponse } from "@/types/browser.types"; - -export const runtime = 'nodejs'; - -const AgentSchema = z.object({ - startUrl: z.string().url().refine((url) => url.startsWith("https://") || url.startsWith("http://"), "startUrl must be http or https"), - task: z.string().min(1, "task is required"), -}); - -export async function POST(req: NextRequest) { - let raw: unknown; - try { - raw = await req.json(); - } catch { - return NextResponse.json( - { success: false, error: "Invalid JSON body" } as BrowserAgentResponse, - { status: 400 } - ); - } - - const parsed = AgentSchema.safeParse(raw); - if (!parsed.success) { - return NextResponse.json( - { - success: false, - error: parsed.error.flatten().formErrors.join(", ") || "Invalid request" - } as BrowserAgentResponse, - { status: 400 } - ); - } - - const { startUrl, task } = parsed.data; - - if (isBlockedStartUrl(startUrl)) { - return NextResponse.json( - { - success: false, - error: "startUrl points to a private or disallowed host" - } as BrowserAgentResponse, - { status: 400 } - ); - } - - // Note: This route is deprecated/simplified - for full multi-step agent, use browser_agent tool directly - // This route provides a simple HTTP interface for single-action browser automation - try { - const result = await withBrowser(async (page) => { - await page.goto(startUrl, { waitUntil: "domcontentloaded", timeout: 20000 }); - return await page.act(task); - }); - - const success = hasBooleanSuccess(result) ? result.success : true; - const out = typeof result === "string" ? result : JSON.stringify(result); - - return NextResponse.json( - { success, result: out }, - { status: hasBooleanSuccess(result) && !result.success ? 422 : 200 } - ); - } catch (error) { - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - } as BrowserAgentResponse, - { status: 500 } - ); - } -} - -export const dynamic = "force-dynamic"; -export const revalidate = 0; -export const fetchCache = "force-no-store"; -export const maxDuration = 300; - diff --git a/app/api/browser/extract/route.ts b/app/api/browser/extract/route.ts deleted file mode 100644 index 7ecad2a12..000000000 --- a/app/api/browser/extract/route.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { withBrowser } from "@/lib/browser/withBrowser"; -import { schemaToZod } from "@/lib/browser/schemaToZod"; -import { isBlockedStartUrl } from "@/lib/browser/isBlockedStartUrl"; -import type { BrowserExtractResponse } from "@/types/browser.types"; - -export const runtime = 'nodejs'; - -const ExtractSchema = z.object({ - url: z.string().url(), - schema: z.record(z.string()), - instruction: z.string().optional(), -}); - -export async function POST(req: NextRequest) { - let raw: unknown; - try { - raw = await req.json(); - } catch { - return NextResponse.json( - { success: false, error: "Invalid JSON body" } as BrowserExtractResponse, - { status: 400 } - ); - } - - const parsed = ExtractSchema.safeParse(raw); - if (!parsed.success) { - return NextResponse.json( - { - success: false, - error: parsed.error.flatten().formErrors.join(", ") || "Invalid request" - } as BrowserExtractResponse, - { status: 400 } - ); - } - - const { url, schema, instruction } = parsed.data; - - if (isBlockedStartUrl(url)) { - return NextResponse.json( - { - success: false, - error: "url points to a private or disallowed host" - } as BrowserExtractResponse, - { status: 400 } - ); - } - - try { - - const data = await withBrowser(async (page) => { - await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 }); - - const zodSchema = schemaToZod(schema); - - return await page.extract({ - instruction: instruction || `Extract data according to the provided schema`, - schema: zodSchema, - }); - }); - - return NextResponse.json({ - success: true, - data, - } as BrowserExtractResponse); - } catch (error) { - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - } as BrowserExtractResponse, - { status: 500 } - ); - } -} - -export const dynamic = "force-dynamic"; -export const revalidate = 0; -export const fetchCache = "force-no-store"; - diff --git a/app/api/browser/observe/route.ts b/app/api/browser/observe/route.ts deleted file mode 100644 index d9cb1fdbb..000000000 --- a/app/api/browser/observe/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { withBrowser } from "@/lib/browser/withBrowser"; -import { isBlockedStartUrl } from "@/lib/browser/isBlockedStartUrl"; -import type { BrowserObserveResponse } from "@/types/browser.types"; - -export const runtime = 'nodejs'; - -const ObserveSchema = z.object({ - url: z.string().url(), - instruction: z.string().optional(), -}); - -export async function POST(req: NextRequest) { - let raw: unknown; - try { - raw = await req.json(); - } catch { - return NextResponse.json( - { success: false, error: "Invalid JSON body" } as BrowserObserveResponse, - { status: 400 } - ); - } - - const parsed = ObserveSchema.safeParse(raw); - if (!parsed.success) { - return NextResponse.json( - { - success: false, - error: parsed.error.flatten().formErrors.join(", ") || "Invalid request" - } as BrowserObserveResponse, - { status: 400 } - ); - } - - const { url, instruction } = parsed.data; - - if (isBlockedStartUrl(url)) { - return NextResponse.json( - { - success: false, - error: "url points to a private or disallowed host" - } as BrowserObserveResponse, - { status: 400 } - ); - } - - try { - - const observeResult = await withBrowser(async (page) => { - await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 }); - - return await page.observe({ - instruction: instruction || "Find all interactive elements and actions", - }); - }); - - const actionsArray = Array.isArray(observeResult) - ? observeResult - : [observeResult]; - - const actions = actionsArray.map(result => - typeof result === "string" ? result : JSON.stringify(result) - ); - - return NextResponse.json({ - success: true, - actions, - } as BrowserObserveResponse); - } catch (error) { - return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - } as BrowserObserveResponse, - { status: 500 } - ); - } -} - -export const dynamic = "force-dynamic"; -export const revalidate = 0; -export const fetchCache = "force-no-store"; - diff --git a/lib/browser/hasBooleanSuccess.ts b/lib/browser/hasBooleanSuccess.ts deleted file mode 100644 index 686029121..000000000 --- a/lib/browser/hasBooleanSuccess.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Type guard: Check if a value has a boolean success property - * Used to properly type-check browser operation results - */ -export function hasBooleanSuccess(x: unknown): x is { success: boolean } { - return ( - typeof x === "object" && - x !== null && - "success" in x && - typeof (x as { success: unknown }).success === "boolean" - ); -} - diff --git a/types/browser.types.ts b/types/browser.types.ts deleted file mode 100644 index 33519c91c..000000000 --- a/types/browser.types.ts +++ /dev/null @@ -1,60 +0,0 @@ -export interface BrowserActRequest { - url: string; - action: string; -} - -export interface BrowserActResponse { - success: boolean; - message?: string; - error?: string; - screenshotUrl?: string; - sessionUrl?: string; - platformName?: string; -} - -export interface BrowserExtractRequest { - url: string; - schema: Record; - instruction?: string; -} - -export interface BrowserExtractResponse { - success: boolean; - data?: unknown; - error?: string; - initialScreenshotUrl?: string; - finalScreenshotUrl?: string; - sessionUrl?: string; - platformName?: string; -} - -export interface BrowserObserveRequest { - url: string; - instruction?: string; -} - -export interface BrowserObserveResponse { - success: boolean; - actions?: string[]; - message?: string; - error?: string; - screenshotUrl?: string; - sessionUrl?: string; - platformName?: string; -} - -export interface BrowserAgentRequest { - startUrl: string; - task: string; - model?: string; -} - -export interface BrowserAgentResponse { - success: boolean; - result?: string; - error?: string; - screenshotUrl?: string; - sessionUrl?: string; - platformName?: string; -} - From 74bf97ccb5d78adb884579533a3f7c1dec2cd2d0 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 10:41:19 -0500 Subject: [PATCH 2/9] chore: delete unused /api/funnel_analysis/comments endpoint Remove unused funnel_analysis/comments API endpoint and its orphaned helper function. Grep confirmed no usages in codebase. Files deleted: - app/api/funnel_analysis/comments/route.ts - lib/supabase/getFunnelAnalyticsComments.tsx Co-Authored-By: Claude Opus 4.5 --- app/api/funnel_analysis/comments/route.ts | 20 -------------------- lib/supabase/getFunnelAnalyticsComments.tsx | 20 -------------------- 2 files changed, 40 deletions(-) delete mode 100644 app/api/funnel_analysis/comments/route.ts delete mode 100644 lib/supabase/getFunnelAnalyticsComments.tsx diff --git a/app/api/funnel_analysis/comments/route.ts b/app/api/funnel_analysis/comments/route.ts deleted file mode 100644 index 868555f4c..000000000 --- a/app/api/funnel_analysis/comments/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import getFunnelAnalyticsComments from "@/lib/supabase/getFunnelAnalyticsComments"; -import { NextRequest } from "next/server"; - -export async function POST(req: NextRequest) { - const body = await req.json(); - const chatIds = body.chatIds; - - try { - const data = await getFunnelAnalyticsComments(chatIds); - return Response.json({ data }, { status: 200 }); - } catch (error) { - console.error(error); - const message = error instanceof Error ? error.message : "failed"; - return Response.json({ message }, { status: 400 }); - } -} - -export const dynamic = "force-dynamic"; -export const fetchCache = "force-no-store"; -export const revalidate = 0; diff --git a/lib/supabase/getFunnelAnalyticsComments.tsx b/lib/supabase/getFunnelAnalyticsComments.tsx deleted file mode 100644 index 7d4eef897..000000000 --- a/lib/supabase/getFunnelAnalyticsComments.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import supabase from "./serverClient"; - -const getFunnelAnalyticsComments = async (ids: Array) => { - const { data: analytics } = await supabase - .from("funnel_analytics") - .select("*") - .in("chat_id", ids); - - const analysesIds = analytics?.map((analysis) => analysis.id); - - const { data } = await supabase - .from("funnel_analytics_comments") - .select("*") - .in("analysis_id", analysesIds || []) - .order("timestamp", { ascending: false }); - - return data; -}; - -export default getFunnelAnalyticsComments; From 6f8afe7401904168bd0d9ff391d5b52e1f31b1cf Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 10:46:54 -0500 Subject: [PATCH 3/9] chore: delete unused /api/get_fans_profiles endpoint Co-Authored-By: Claude Opus 4.5 --- app/api/get_fans_profiles/route.ts | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 app/api/get_fans_profiles/route.ts diff --git a/app/api/get_fans_profiles/route.ts b/app/api/get_fans_profiles/route.ts deleted file mode 100644 index f9b43cf36..000000000 --- a/app/api/get_fans_profiles/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import supabase from "@/lib/supabase/serverClient"; -import { NextRequest } from "next/server"; - -export async function GET(req: NextRequest) { - const artistId = req.nextUrl.searchParams.get("artistId"); - try { - const { data } = await supabase - .from("fans_segments") - .select("*") - .eq("artistId", artistId); - - return Response.json({ data }, { status: 200 }); - } catch (error) { - console.error(error); - const message = error instanceof Error ? error.message : "failed"; - return Response.json({ message }, { status: 400 }); - } -} - -export const dynamic = "force-dynamic"; -export const fetchCache = "force-no-store"; -export const revalidate = 0; From e22ac1204e82433b63c43e90927b43f4cc7ef736 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 11:36:20 -0500 Subject: [PATCH 4/9] feat: migrate chat creation to recoup-api - Update useCreateChat hook to call recoup-api /api/chats endpoint directly - Use Bearer token auth with Privy access token - Add RFC 8594 deprecation headers to old /api/chat/create endpoint - Set 90-day sunset period for deprecated endpoint This completes the client-side migration. The old endpoint remains functional during the sunset period but clients should use recoup-api. Co-Authored-By: Claude Opus 4.5 --- app/api/chat/create/route.ts | 23 +++++++++++++++++ hooks/useCreateChat.tsx | 48 +++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/app/api/chat/create/route.ts b/app/api/chat/create/route.ts index 56aba678d..450b069cd 100644 --- a/app/api/chat/create/route.ts +++ b/app/api/chat/create/route.ts @@ -3,8 +3,27 @@ import { createRoomWithReport } from "@/lib/supabase/createRoomWithReport"; import { generateChatTitle } from "@/lib/chat/generateChatTitle"; import { sendNewConversationNotification } from "@/lib/telegram/sendNewConversationNotification"; import { CreateChatRequest } from "@/types/Chat"; +import { NEW_API_BASE_URL } from "@/lib/consts"; +const SUNSET_DAYS = 90; + +function getDeprecationHeaders(): Record { + const sunsetDate = new Date(); + sunsetDate.setDate(sunsetDate.getDate() + SUNSET_DAYS); + + return { + Deprecation: "true", + Sunset: sunsetDate.toUTCString(), + Link: `<${NEW_API_BASE_URL}/api/chats>; rel="deprecation"`, + }; +} + +/** + * @deprecated This endpoint is deprecated. Use recoup-api directly at recoup-api.vercel.app/api/chats + */ export async function POST(request: NextRequest) { + const deprecationHeaders = getDeprecationHeaders(); + try { const body: CreateChatRequest = await request.json(); @@ -18,6 +37,7 @@ export async function POST(request: NextRequest) { }, { status: 400, + headers: deprecationHeaders, } ); } @@ -42,6 +62,7 @@ export async function POST(request: NextRequest) { }, { status: 500, + headers: deprecationHeaders, } ); } @@ -67,6 +88,7 @@ export async function POST(request: NextRequest) { }, { status: 200, + headers: deprecationHeaders, } ); } catch (error) { @@ -78,6 +100,7 @@ export async function POST(request: NextRequest) { }, { status: 500, + headers: deprecationHeaders, } ); } diff --git a/hooks/useCreateChat.tsx b/hooks/useCreateChat.tsx index fa4361790..1f746ebd7 100644 --- a/hooks/useCreateChat.tsx +++ b/hooks/useCreateChat.tsx @@ -1,13 +1,28 @@ import { useEffect } from "react"; -import { - Conversation, - CreateChatRequest, - CreateChatResponse, -} from "@/types/Chat"; +import { Conversation } from "@/types/Chat"; import type { ArtistAgent } from "@/lib/supabase/getArtistAgents"; import { useUserProvider } from "@/providers/UserProvder"; import { useArtistProvider } from "@/providers/ArtistProvider"; import { useConversationsProvider } from "@/providers/ConversationsProvider"; +import { useAccessToken } from "@/hooks/useAccessToken"; +import { NEW_API_BASE_URL } from "@/lib/consts"; + +interface CreateChatApiRequest { + artistId?: string; + chatId?: string; + firstMessage?: string; +} + +interface CreateChatApiResponse { + status: "success" | "error"; + chat?: { + id: string; + topic: string | null; + account_id: string; + artist_id: string | null; + }; + message?: string; +} const useCreateChat = ({ isOptimisticChatItem, @@ -18,12 +33,14 @@ const useCreateChat = ({ chatRoom: Conversation | ArtistAgent; setDisplayName: (displayName: string) => void; }) => { - const { userData, email } = useUserProvider(); + const { userData } = useUserProvider(); const { selectedArtist } = useArtistProvider(); const { refetchConversations } = useConversationsProvider(); + const accessToken = useAccessToken(); useEffect(() => { if (!isOptimisticChatItem) return; + if (!accessToken) return; const createChat = async () => { try { @@ -59,33 +76,34 @@ const useCreateChat = ({ return; } - const requestBody: CreateChatRequest = { - accountId: userData?.account_id || "", + const requestBody: CreateChatApiRequest = { artistId: selectedArtist?.account_id, chatId: (chatRoom as Conversation).id, firstMessage: messageText, - email, }; - const response = await fetch("/api/chat/create", { + const response = await fetch(`${NEW_API_BASE_URL}/api/chats`, { method: "POST", headers: { "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(requestBody), }); - const data: CreateChatResponse = await response.json(); + const data: CreateChatApiResponse = await response.json(); - if (data.success && data.room) { + if (data.status === "success" && data.chat) { // Update display name with the room topic - setDisplayName(data.room.topic); + if (data.chat.topic) { + setDisplayName(data.chat.topic); + } // Remove optimistic flag from memory and treat it as a normal memory. // It will re-enable 3 dots on the chat item. await refetchConversations(); } else { - console.error("Failed to create chat:", data.error); + console.error("Failed to create chat:", data.message); } } catch (error) { console.error("Error creating optimistic chat:", error); @@ -99,7 +117,7 @@ const useCreateChat = ({ userData?.account_id, selectedArtist?.account_id, setDisplayName, - email, + accessToken, refetchConversations, ]); }; From 120f2838bc911012fd25d3ed77681512db96185c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 11:56:08 -0500 Subject: [PATCH 5/9] feat: migrate useArtistAgents to call recoup-api Update useArtistAgents hook to call recoup-api's /api/artist-agents endpoint directly with Bearer token authentication. - Updated hook to use NEW_API_BASE_URL and Bearer token auth - Added deprecation headers to legacy /api/agents endpoint (RFC 8594) - Legacy endpoint still functional during 90-day sunset period Co-Authored-By: Claude Opus 4.5 --- app/api/agents/route.ts | 25 +++++++++++++++++--- hooks/useArtistAgents.ts | 49 ++++++++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/app/api/agents/route.ts b/app/api/agents/route.ts index 23a666ec0..191cc89f3 100644 --- a/app/api/agents/route.ts +++ b/app/api/agents/route.ts @@ -1,25 +1,44 @@ import { NextRequest, NextResponse } from "next/server"; import { getArtistAgents } from "@/lib/supabase/getArtistAgents"; +import { NEW_API_BASE_URL } from "@/lib/consts"; +const SUNSET_DAYS = 90; + +function getDeprecationHeaders(): Record { + const sunsetDate = new Date(); + sunsetDate.setDate(sunsetDate.getDate() + SUNSET_DAYS); + + return { + Deprecation: "true", + Sunset: sunsetDate.toUTCString(), + Link: `<${NEW_API_BASE_URL}/api/artist-agents>; rel="deprecation"`, + }; +} + +/** + * @deprecated This endpoint is deprecated. Use recoup-api directly at recoup-api.vercel.app/api/artist-agents + */ export async function GET(request: NextRequest) { + const deprecationHeaders = getDeprecationHeaders(); + const { searchParams } = new URL(request.url); const socialIds = searchParams.getAll("socialId"); if (!socialIds.length) { return NextResponse.json( { error: "At least one Social ID is required" }, - { status: 400 }, + { status: 400, headers: deprecationHeaders }, ); } try { const agents = await getArtistAgents(socialIds); - return NextResponse.json(agents); + return NextResponse.json(agents, { headers: deprecationHeaders }); } catch (error) { console.error("Error fetching segments:", error); return NextResponse.json( { error: "Failed to fetch segments" }, - { status: 500 }, + { status: 500, headers: deprecationHeaders }, ); } } diff --git a/hooks/useArtistAgents.ts b/hooks/useArtistAgents.ts index f268e653b..723074c7e 100644 --- a/hooks/useArtistAgents.ts +++ b/hooks/useArtistAgents.ts @@ -2,26 +2,55 @@ import { ArtistAgent } from "@/lib/supabase/getArtistAgents"; import { useArtistProvider } from "@/providers/ArtistProvider"; import { SOCIAL } from "@/types/Agent"; import { useEffect, useState } from "react"; +import { useAccessToken } from "@/hooks/useAccessToken"; +import { NEW_API_BASE_URL } from "@/lib/consts"; + +interface ArtistAgentsApiResponse { + status: "success" | "error"; + agents?: ArtistAgent[]; + message?: string; +} const useArtistAgents = () => { const [agents, setAgents] = useState([]); const { selectedArtist } = useArtistProvider(); + const accessToken = useAccessToken(); useEffect(() => { const getAgents = async () => { - if (selectedArtist) { - const socialIds = selectedArtist.account_socials?.map( - (social: SOCIAL) => social.id - ); - const queryString = socialIds?.map((id) => `socialId=${id}`).join("&"); - const response = await fetch(`/api/agents?${queryString}`); - const data = await response.json(); - if (data.error) return; - setAgents(data); + if (!selectedArtist) return; + if (!accessToken) return; + + const socialIds = selectedArtist.account_socials?.map( + (social: SOCIAL) => social.id + ); + + if (!socialIds?.length) return; + + const queryString = socialIds.map((id) => `socialId=${id}`).join("&"); + + const response = await fetch( + `${NEW_API_BASE_URL}/api/artist-agents?${queryString}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + const data: ArtistAgentsApiResponse = await response.json(); + + if (data.status === "error") { + console.error("Failed to fetch artist agents:", data.message); + return; + } + + if (data.agents) { + setAgents(data.agents); } }; getAgents(); - }, [selectedArtist]); + }, [selectedArtist, accessToken]); return { agents, From 61cdbb0873ac2e3858e360893929ec228778ad2a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 12:40:43 -0500 Subject: [PATCH 6/9] feat: migrate agent templates fetching to recoup-api - Update fetchAgentTemplates to call recoup-api with Bearer token - Add accessToken to useAgentData hook - Add RFC 8594 deprecation headers to legacy GET endpoint Co-Authored-By: Claude Opus 4.5 --- app/api/agent-templates/route.ts | 23 ++++++++++++-- components/Agents/useAgentData.ts | 10 +++--- lib/agent-templates/fetchAgentTemplates.ts | 37 +++++++++++++++++++--- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/app/api/agent-templates/route.ts b/app/api/agent-templates/route.ts index c594cd80f..985094cd1 100644 --- a/app/api/agent-templates/route.ts +++ b/app/api/agent-templates/route.ts @@ -6,10 +6,29 @@ import { updateAgentTemplate } from "@/lib/supabase/agent_templates/updateAgentT import { deleteAgentTemplate } from "@/lib/supabase/agent_templates/deleteAgentTemplate"; import { verifyAgentTemplateOwner } from "@/lib/supabase/agent_templates/verifyAgentTemplateOwner"; import { getSharedEmailsForTemplates } from "@/lib/supabase/agent_templates/getSharedEmailsForTemplates"; +import { NEW_API_BASE_URL } from "@/lib/consts"; export const runtime = "edge"; +const SUNSET_DAYS = 90; + +function getDeprecationHeaders(): Record { + const sunsetDate = new Date(); + sunsetDate.setDate(sunsetDate.getDate() + SUNSET_DAYS); + + return { + Deprecation: "true", + Sunset: sunsetDate.toUTCString(), + Link: `<${NEW_API_BASE_URL}/api/agent-templates>; rel="deprecation"`, + }; +} + +/** + * @deprecated This endpoint is deprecated. Use recoup-api directly at recoup-api.vercel.app/api/agent-templates + */ export async function GET(request: Request) { + const deprecationHeaders = getDeprecationHeaders(); + try { const { searchParams } = new URL(request.url); const userId = searchParams.get("userId"); @@ -32,10 +51,10 @@ export async function GET(request: Request) { shared_emails: template.is_private ? (sharedEmails[template.id] || []) : [] })); - return NextResponse.json(templatesWithEmails); + return NextResponse.json(templatesWithEmails, { headers: deprecationHeaders }); } catch (error) { console.error('Error fetching agent templates:', error); - return NextResponse.json({ error: 'Failed to fetch templates' }, { status: 500 }); + return NextResponse.json({ error: 'Failed to fetch templates' }, { status: 500, headers: deprecationHeaders }); } } diff --git a/components/Agents/useAgentData.ts b/components/Agents/useAgentData.ts index b148ab2a2..ad7e9f012 100644 --- a/components/Agents/useAgentData.ts +++ b/components/Agents/useAgentData.ts @@ -3,12 +3,14 @@ import { useEffect, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import type { AgentTemplateRow } from "@/types/AgentTemplates"; import fetchAgentTemplates from "@/lib/agent-templates/fetchAgentTemplates"; +import { useAccessToken } from "@/hooks/useAccessToken"; export type Agent = AgentTemplateRow; export function useAgentData() { const { userData } = useUserProvider(); const queryClient = useQueryClient(); + const accessToken = useAccessToken(); const [agents, setAgents] = useState([]); const [selectedTag, setSelectedTag] = useState("Recommended"); const [tags, setTags] = useState(["Recommended"]); @@ -17,9 +19,9 @@ export function useAgentData() { const { data, isPending } = useQuery({ queryKey: ["agent-templates"], - queryFn: () => fetchAgentTemplates(userData!), + queryFn: () => fetchAgentTemplates(userData!, accessToken), retry: 1, - enabled: !!userData?.id, + enabled: !!userData?.id && !!accessToken, }); useEffect(() => { @@ -64,10 +66,10 @@ export function useAgentData() { // Prefetch agent data for better performance on hover const prefetchAgents = () => { - if (userData?.id) { + if (userData?.id && accessToken) { queryClient.prefetchQuery({ queryKey: ["agent-templates"], - queryFn: () => fetchAgentTemplates(userData), + queryFn: () => fetchAgentTemplates(userData, accessToken), staleTime: 5 * 60 * 1000, // 5 minutes }); } diff --git a/lib/agent-templates/fetchAgentTemplates.ts b/lib/agent-templates/fetchAgentTemplates.ts index b43e9f0b2..794702033 100644 --- a/lib/agent-templates/fetchAgentTemplates.ts +++ b/lib/agent-templates/fetchAgentTemplates.ts @@ -1,12 +1,41 @@ import type { AgentTemplateRow } from "@/types/AgentTemplates"; import type { AccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails"; +import { NEW_API_BASE_URL } from "@/lib/consts"; + +interface AgentTemplatesApiResponse { + status: "success" | "error"; + templates?: AgentTemplateRow[]; + message?: string; +} const fetchAgentTemplates = async ( - userData: AccountWithDetails + userData: AccountWithDetails, + accessToken: string | null ): Promise => { - const res = await fetch(`/api/agent-templates?userId=${userData?.id}`); - if (!res.ok) throw new Error("Failed to fetch agent templates"); - return (await res.json()) as AgentTemplateRow[]; + if (!accessToken) { + throw new Error("No access token available"); + } + + const res = await fetch( + `${NEW_API_BASE_URL}/api/agent-templates?userId=${userData?.id}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + if (!res.ok) { + throw new Error("Failed to fetch agent templates"); + } + + const data: AgentTemplatesApiResponse = await res.json(); + + if (data.status === "error") { + throw new Error(data.message || "Failed to fetch agent templates"); + } + + return data.templates || []; }; export default fetchAgentTemplates; From ce8e8dfa79e96bf4b8e85329b7ea59e724fa0e08 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 14:04:08 -0500 Subject: [PATCH 7/9] feat: migrate agent-templates/favorites client to recoup-api - Update useAgentToggleFavorite.ts to call recoup-api with Bearer token - Add RFC 8594 deprecation headers to old favorites endpoint Co-Authored-By: Claude Opus 4.5 --- app/api/agent-templates/favorites/route.ts | 34 +++++++++++++++++-- components/Agents/useAgentToggleFavorite.ts | 37 ++++++++++++--------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/app/api/agent-templates/favorites/route.ts b/app/api/agent-templates/favorites/route.ts index d1fb81729..e4b369c24 100644 --- a/app/api/agent-templates/favorites/route.ts +++ b/app/api/agent-templates/favorites/route.ts @@ -2,15 +2,37 @@ import { NextResponse } from "next/server"; import { addAgentTemplateFavorite } from "@/lib/supabase/agent_templates/addAgentTemplateFavorite"; import { removeAgentTemplateFavorite } from "@/lib/supabase/agent_templates/removeAgentTemplateFavorite"; import type { ToggleFavoriteRequest, ToggleFavoriteResponse } from "@/types/AgentTemplates"; +import { NEW_API_BASE_URL } from "@/lib/consts"; export const runtime = "edge"; +const SUNSET_DAYS = 90; + +function getDeprecationHeaders(): Record { + const sunsetDate = new Date(); + sunsetDate.setDate(sunsetDate.getDate() + SUNSET_DAYS); + + return { + Deprecation: "true", + Sunset: sunsetDate.toUTCString(), + Link: `<${NEW_API_BASE_URL}/api/agent-templates/favorites>; rel="deprecation"`, + }; +} + +/** + * @deprecated This endpoint is deprecated. Use recoup-api directly at recoup-api.vercel.app/api/agent-templates/favorites + */ export async function POST(request: Request) { + const deprecationHeaders = getDeprecationHeaders(); + try { const { templateId, userId, isFavourite }: ToggleFavoriteRequest = await request.json(); if (!templateId || !userId) { - return NextResponse.json({ error: "Missing templateId or userId" }, { status: 400 }); + return NextResponse.json( + { error: "Missing templateId or userId" }, + { status: 400, headers: deprecationHeaders } + ); } if (isFavourite) { @@ -19,10 +41,16 @@ export async function POST(request: Request) { await removeAgentTemplateFavorite(templateId, userId); } - return NextResponse.json({ success: true } as ToggleFavoriteResponse); + return NextResponse.json( + { success: true } as ToggleFavoriteResponse, + { headers: deprecationHeaders } + ); } catch (error) { console.error("Error toggling favourite:", error); - return NextResponse.json({ error: "Failed to toggle favourite" } as ToggleFavoriteResponse, { status: 500 }); + return NextResponse.json( + { error: "Failed to toggle favourite" } as ToggleFavoriteResponse, + { status: 500, headers: deprecationHeaders } + ); } } diff --git a/components/Agents/useAgentToggleFavorite.ts b/components/Agents/useAgentToggleFavorite.ts index 0d87717b1..b191c7415 100644 --- a/components/Agents/useAgentToggleFavorite.ts +++ b/components/Agents/useAgentToggleFavorite.ts @@ -1,38 +1,45 @@ import { useUserProvider } from "@/providers/UserProvder"; import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import type { ToggleFavoriteRequest } from "@/types/AgentTemplates"; +import { useAccessToken } from "@/hooks/useAccessToken"; +import { NEW_API_BASE_URL } from "@/lib/consts"; export function useAgentToggleFavorite() { const { userData } = useUserProvider(); const queryClient = useQueryClient(); + const accessToken = useAccessToken(); const handleToggleFavorite = async ( templateId: string, nextFavourite: boolean ) => { if (!userData?.id || !templateId) return; - + if (!accessToken) return; + try { - const body: ToggleFavoriteRequest = { - templateId, - userId: userData.id, - isFavourite: nextFavourite, - }; - const res = await fetch("/api/agent-templates/favorites", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - + const res = await fetch( + `${NEW_API_BASE_URL}/api/agent-templates/favorites`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + templateId, + isFavourite: nextFavourite, + }), + } + ); + if (!res.ok) { throw new Error("Failed to toggle favorite"); } - + toast.success( nextFavourite ? "Added to favorites" : "Removed from favorites" ); - + // Invalidate templates list so is_favourite and favorites_count refresh queryClient.invalidateQueries({ queryKey: ["agent-templates"] }); } catch { From cc20ef4b707ae619b7cb627f991eb9433af2a896 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 14:13:21 -0500 Subject: [PATCH 8/9] feat: migrate agent-creator client to recoup-api Update AgentCreator.tsx to fetch from recoup-api directly. Add RFC 8594 deprecation headers to the old /api/agent-creator endpoint. Co-Authored-By: Claude Opus 4.5 --- app/api/agent-creator/route.ts | 39 +++++++++++++++++++++++++----- components/Agents/AgentCreator.tsx | 6 ++++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/app/api/agent-creator/route.ts b/app/api/agent-creator/route.ts index 8380a329c..30f7f6f81 100644 --- a/app/api/agent-creator/route.ts +++ b/app/api/agent-creator/route.ts @@ -1,21 +1,45 @@ import getAccountById from "@/lib/supabase/accounts/getAccountById"; import { ADMIN_EMAILS } from "@/lib/admin"; -import { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; +import { NEW_API_BASE_URL } from "@/lib/consts"; export const runtime = "edge"; +const SUNSET_DAYS = 90; + +function getDeprecationHeaders(): Record { + const sunsetDate = new Date(); + sunsetDate.setDate(sunsetDate.getDate() + SUNSET_DAYS); + + return { + Deprecation: "true", + Sunset: sunsetDate.toUTCString(), + Link: `<${NEW_API_BASE_URL}/api/agent-creator>; rel="deprecation"`, + }; +} + +/** + * @deprecated This endpoint is deprecated. Use recoup-api directly at recoup-api.vercel.app/api/agent-creator + */ export async function GET(req: NextRequest) { + const deprecationHeaders = getDeprecationHeaders(); const creatorId = req.nextUrl.searchParams.get("creatorId"); if (!creatorId) { - return Response.json({ message: "Missing creatorId" }, { status: 400 }); + return NextResponse.json( + { message: "Missing creatorId" }, + { status: 400, headers: deprecationHeaders }, + ); } try { const account = await getAccountById(creatorId); if (!account) { - return Response.json({ message: "Creator not found" }, { status: 404 }); + return NextResponse.json( + { message: "Creator not found" }, + { status: 404, headers: deprecationHeaders }, + ); } const info = Array.isArray(account.account_info) @@ -26,7 +50,7 @@ export async function GET(req: NextRequest) { : null; const isAdmin = !!email && ADMIN_EMAILS.includes(email); - return Response.json( + return NextResponse.json( { creator: { name: account.name || null, @@ -34,11 +58,14 @@ export async function GET(req: NextRequest) { is_admin: isAdmin, }, }, - { status: 200 }, + { status: 200, headers: deprecationHeaders }, ); } catch (e) { const message = e instanceof Error ? e.message : "failed"; - return Response.json({ message }, { status: 400 }); + return NextResponse.json( + { message }, + { status: 400, headers: deprecationHeaders }, + ); } } diff --git a/components/Agents/AgentCreator.tsx b/components/Agents/AgentCreator.tsx index 77a6c566b..f1dc3aeca 100644 --- a/components/Agents/AgentCreator.tsx +++ b/components/Agents/AgentCreator.tsx @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import Image from "next/image"; import { Avatar, AvatarImage } from "@/components/ui/avatar"; +import { NEW_API_BASE_URL } from "@/lib/consts"; interface AgentCreatorProps { creatorId: string | null; @@ -21,7 +22,10 @@ const AgentCreator = ({ creatorId, className }: AgentCreatorProps) => { const { data } = useQuery({ queryKey: ["agent-creator", creatorId], queryFn: async () => { - const res = await fetch(`/api/agent-creator?creatorId=${creatorId}`, { cache: "no-store" }); + const res = await fetch( + `${NEW_API_BASE_URL}/api/agent-creator?creatorId=${creatorId}`, + { cache: "no-store" } + ); if (!res.ok) throw new Error("failed"); return (await res.json()) as CreatorResponse; }, From f93792e1c53a8f8e5ed29ebd69173ed64150ab28 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 16 Jan 2026 15:33:00 -0500 Subject: [PATCH 9/9] feat: migrate useAvailableModels to recoup-api Update client to fetch AI models from recoup-api instead of local endpoint. Add RFC 8594 deprecation headers to local endpoint for 90-day sunset. Co-Authored-By: Claude Opus 4.5 --- app/api/ai/models/route.ts | 23 ++++++++++++++++++++--- hooks/useAvailableModels.ts | 3 ++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/api/ai/models/route.ts b/app/api/ai/models/route.ts index f901d54d2..bfa25d7be 100644 --- a/app/api/ai/models/route.ts +++ b/app/api/ai/models/route.ts @@ -1,20 +1,37 @@ import { getAvailableModels } from "@/lib/ai/getAvailableModels"; +import { NEW_API_BASE_URL } from "@/lib/consts"; + +const SUNSET_DAYS = 90; + +function getDeprecationHeaders(): Record { + const sunsetDate = new Date(); + sunsetDate.setDate(sunsetDate.getDate() + SUNSET_DAYS); + + return { + Deprecation: "true", + Sunset: sunsetDate.toUTCString(), + Link: `<${NEW_API_BASE_URL}/api/ai/models>; rel="deprecation"`, + }; +} /** * GET /api/ai/models * + * @deprecated This endpoint is deprecated. Use recoup-api directly at recoup-api.vercel.app/api/ai/models + * * Server-side endpoint that proxies `getAvailableModels()` so that the * client can fetch model metadata without requiring server-side imports * of `@ai-sdk/gateway`. */ export async function GET() { + const deprecationHeaders = getDeprecationHeaders(); + try { const models = await getAvailableModels(); - return Response.json({ models }); + return Response.json({ models }, { headers: deprecationHeaders }); } catch (error) { - console.error("/api/ai/models error", error); const message = error instanceof Error ? error.message : "failed"; - return Response.json({ message }, { status: 500 }); + return Response.json({ message }, { status: 500, headers: deprecationHeaders }); } } diff --git a/hooks/useAvailableModels.ts b/hooks/useAvailableModels.ts index de7d35196..6349f26da 100644 --- a/hooks/useAvailableModels.ts +++ b/hooks/useAvailableModels.ts @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { GatewayLanguageModelEntry } from "@ai-sdk/gateway"; +import { NEW_API_BASE_URL } from "@/lib/consts"; /** * A thin wrapper around TanStack's useQuery to fetch the list of @@ -9,7 +10,7 @@ const useAvailableModels = () => useQuery({ queryKey: ["available-models"], queryFn: async () => { - const res = await fetch("/api/ai/models"); + const res = await fetch(`${NEW_API_BASE_URL}/api/ai/models`); if (!res.ok) throw new Error("Failed to load models"); const data = (await res.json()) as { models: GatewayLanguageModelEntry[];