diff --git a/src/client/features/saved-keywords/SavedKeywordsHeader.tsx b/src/client/features/saved-keywords/SavedKeywordsHeader.tsx
index fff85a0..6ff5e74 100644
--- a/src/client/features/saved-keywords/SavedKeywordsHeader.tsx
+++ b/src/client/features/saved-keywords/SavedKeywordsHeader.tsx
@@ -1,15 +1,26 @@
-import { ChevronDown, Download, FileDown, Loader2, Sheet } from "lucide-react";
+import {
+ ChevronDown,
+ Download,
+ FileDown,
+ Loader2,
+ RefreshCw,
+ Sheet,
+} from "lucide-react";
export function SavedKeywordsHeader({
totalCount,
exporting,
+ metricsRefreshing,
onExportCsv,
onExportSheets,
+ onRefreshMetrics,
}: {
totalCount: number;
exporting: "csv" | "sheets" | null;
+ metricsRefreshing: boolean;
onExportCsv: () => void;
onExportSheets: () => void;
+ onRefreshMetrics: () => void;
}) {
const disabled = totalCount === 0 || exporting != null;
@@ -23,40 +34,83 @@ export function SavedKeywordsHeader({
- void exporter.exportFilteredCsv()}
onExportSheets={() => void exporter.exportFilteredSheets()}
+ onRefreshMetrics={() => refreshMetricsMutation.mutate()}
/>
diff --git a/src/server/features/keywords/services/KeywordResearchService.ts b/src/server/features/keywords/services/KeywordResearchService.ts
index 98c7488..6f57634 100644
--- a/src/server/features/keywords/services/KeywordResearchService.ts
+++ b/src/server/features/keywords/services/KeywordResearchService.ts
@@ -8,6 +8,7 @@ import {
exportSavedKeywords,
updateSavedKeywordTag,
updateSavedKeywordTags,
+ refreshSavedKeywordMetrics,
} from "@/server/features/keywords/services/research";
export const KeywordResearchService = {
@@ -20,4 +21,5 @@ export const KeywordResearchService = {
updateSavedKeywordTag,
deleteSavedKeywordTag,
removeSavedKeywords,
+ refreshSavedKeywordMetrics,
} as const;
diff --git a/src/server/features/keywords/services/research/index.ts b/src/server/features/keywords/services/research/index.ts
index 8eb7e20..e777f00 100644
--- a/src/server/features/keywords/services/research/index.ts
+++ b/src/server/features/keywords/services/research/index.ts
@@ -9,3 +9,4 @@ export {
deleteSavedKeywordTag,
removeSavedKeywords,
} from "./saved-keywords";
+export { refreshSavedKeywordMetrics } from "./refresh-metrics";
diff --git a/src/server/features/keywords/services/research/refresh-metrics.ts b/src/server/features/keywords/services/research/refresh-metrics.ts
new file mode 100644
index 0000000..337f059
--- /dev/null
+++ b/src/server/features/keywords/services/research/refresh-metrics.ts
@@ -0,0 +1,145 @@
+import { KeywordResearchRepository } from "@/server/features/keywords/repositories/KeywordResearchRepository";
+import { normalizeIntent } from "@/server/features/keywords/services/research/helpers";
+import { createDataforseoClient } from "@/server/lib/dataforseo";
+import type { BillingCustomerContext } from "@/server/billing/subscription";
+import type { KeywordIntent, MonthlySearch } from "@/types/keywords";
+import type { RefreshSavedKeywordMetricsInput } from "@/types/schemas/keywords";
+import { getKeywordDataProvider } from "@/shared/keyword-locations";
+
+const REFRESH_BATCH_SIZE = 700;
+
+// Match the shape the research/save flow persists so a refresh never degrades
+// stored metrics (see research-data.ts mapKeywordDataItems / mapAdsKeywordItems).
+function toMonthlySearchesJson(
+ entries:
+ | {
+ year?: number | null;
+ month?: number | null;
+ search_volume?: number | null;
+ }[]
+ | null
+ | undefined,
+): string {
+ const trend: MonthlySearch[] = (entries ?? []).map((entry) => ({
+ year: entry.year ?? 0,
+ month: entry.month ?? 0,
+ searchVolume: entry.search_volume ?? 0,
+ }));
+ return JSON.stringify(trend);
+}
+
+type RefreshedMetric = {
+ searchVolume: number | null;
+ cpc: number | null;
+ competition: number | null;
+ keywordDifficulty: number | null;
+ intent: KeywordIntent;
+ monthlySearchesJson: string;
+};
+
+// Fetch fresh metrics for one homogeneous batch and key them by lowercase
+// keyword. Labs covers most countries; the rest fall back to Google Ads, which
+// carries volume/CPC/competition but no difficulty or intent.
+async function fetchBatchMetrics(
+ client: ReturnType,
+ request: { keywords: string[]; locationCode: number; languageCode: string },
+ useGoogleAds: boolean,
+): Promise