diff --git a/src/components/home/ActivityFeed.tsx b/src/components/home/ActivityFeed.tsx
new file mode 100644
index 00000000..c540c65d
--- /dev/null
+++ b/src/components/home/ActivityFeed.tsx
@@ -0,0 +1,188 @@
+import { useEffect, useRef } from 'react';
+import { Link } from 'react-router';
+import { useRecentActivity } from '@/hooks/useRecentActivity';
+import { formatRelativeTime } from '@/utils/time.utils';
+import { cn } from '@/lib/utils';
+
+/* ------------------------------------------------------------------ */
+/* Skeleton */
+/* ------------------------------------------------------------------ */
+
+const skeletonBlockClass =
+ 'rounded-md bg-white/12 skeleton-shimmer motion-reduce:bg-white/18 motion-reduce:ring-1 motion-reduce:ring-white/15';
+
+function ActivitySkeletonRow() {
+ return (
+
+ );
+}
+
+function ActivitySkeletonList() {
+ return (
+
+
Loading recent activity
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Row */
+/* ------------------------------------------------------------------ */
+
+interface ActivityRowProps {
+ walletAddress: string;
+ type: 'buy' | 'sell';
+ creatorName: string;
+ creatorId: string;
+ quantity: number;
+ timestamp: number;
+}
+
+function ActivityRow({
+ walletAddress,
+ type,
+ creatorName,
+ creatorId,
+ quantity,
+ timestamp,
+}: ActivityRowProps) {
+ const truncatedWallet = `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`;
+ const actionLabel = type === 'buy' ? 'Bought' : 'Sold';
+ const actionColor =
+ type === 'buy'
+ ? 'text-emerald-600'
+ : 'text-red-500';
+ const timeAgo = formatRelativeTime(timestamp);
+
+ return (
+
+ {/* Wallet */}
+
+ {truncatedWallet}
+
+
+ {/* Action */}
+
+ {actionLabel}
+
+
+ {/* Creator Name */}
+
+ {creatorName}
+
+
+ {/* Quantity */}
+
+ {quantity}
+
+
+ {/* Time */}
+
+ {timeAgo}
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Section */
+/* ------------------------------------------------------------------ */
+
+export default function ActivityFeed() {
+ const headingRef = useRef(null);
+ const listRef = useRef(null);
+
+ const { trades, isLoading } = useRecentActivity();
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add('is-visible');
+ observer.unobserve(entry.target);
+ }
+ });
+ },
+ { threshold: 0.1 }
+ );
+
+ if (headingRef.current) observer.observe(headingRef.current);
+ if (listRef.current) observer.observe(listRef.current);
+
+ return () => observer.disconnect();
+ }, []);
+
+ if (!isLoading && (!trades || trades.length === 0)) {
+ return null;
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ Live Activity
+
+
+
+
+
+ Recent marketplace
+
+
+
+ activity across all creators.
+
+
+
+
+ {/* List or Skeleton */}
+ {isLoading ? (
+
+ ) : (
+
+ {trades?.map(trade => (
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/marketplace/TrendingCreatorsSection.tsx b/src/components/marketplace/TrendingCreatorsSection.tsx
new file mode 100644
index 00000000..a19c5149
--- /dev/null
+++ b/src/components/marketplace/TrendingCreatorsSection.tsx
@@ -0,0 +1,158 @@
+import { Link } from 'react-router';
+import { useTrendingCreators } from '@/hooks/useTrendingCreators';
+import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils';
+import { formatNumber } from '@/utils/numberFormat.utils';
+import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar';
+import Change24hBadge from '@/components/common/Change24hBadge';
+import { cn } from '@/lib/utils';
+import MarketplaceSection from '@/components/common/MarketplaceSection';
+import SectionDivider from '@/components/common/SectionDivider';
+
+/* ------------------------------------------------------------------ */
+/* Skeleton */
+/* ------------------------------------------------------------------ */
+
+const skeletonBlockClass =
+ 'rounded-md bg-white/12 skeleton-shimmer motion-reduce:bg-white/18 motion-reduce:ring-1 motion-reduce:ring-white/15';
+
+function TrendingSkeletonRow() {
+ return (
+
+ );
+}
+
+function TrendingSkeletonList() {
+ return (
+
+ Loading trending creators
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Row */
+/* ------------------------------------------------------------------ */
+
+interface TrendingRowProps {
+ rank: number;
+ name: string;
+ thumbnail?: string;
+ creatorId: string;
+ volume: number;
+ change24h: number;
+}
+
+function TrendingRow({
+ rank,
+ name,
+ thumbnail,
+ creatorId,
+ volume,
+ change24h,
+}: TrendingRowProps) {
+ return (
+
+ {/* Rank */}
+
+ #{rank}
+
+
+ {/* Avatar */}
+
+
+
+
+ {/* Name */}
+
+ {name}
+
+
+ {/* 24h Volume */}
+
+ {formatNumber(volume, { style: 'compact' })} XLM
+
+
+ {/* 24h Change Badge */}
+
+
+
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* Section */
+/* ------------------------------------------------------------------ */
+
+export default function TrendingCreatorsSection() {
+ const { creators, isLoading } = useTrendingCreators();
+
+ if (!isLoading && (!creators || creators.length === 0)) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+ 24-hour momentum
+
+
+ Discover creators with the highest buy volume in the last 24
+ hours — early signals for momentum and opportunity.
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {creators?.map((creator, index) => (
+
+ ))}
+
+ )}
+
+ >
+ );
+}
diff --git a/src/hooks/useRecentActivity.ts b/src/hooks/useRecentActivity.ts
new file mode 100644
index 00000000..6c8a3345
--- /dev/null
+++ b/src/hooks/useRecentActivity.ts
@@ -0,0 +1,33 @@
+import { useQuery } from '@tanstack/react-query';
+import { globalActivityService, type GlobalActivityTrade } from '@/services/globalActivity.service';
+import { queryKeys } from '@/lib/queryKeys';
+
+export const RECENT_ACTIVITY_POLL_INTERVAL_MS = 60_000; // 60 seconds
+export const RECENT_ACTIVITY_DISPLAY_LIMIT = 5;
+
+export interface UseRecentActivityResult {
+ trades: GlobalActivityTrade[];
+ isLoading: boolean;
+ isError: boolean;
+ error: Error | null;
+ refetch: () => void;
+}
+
+export function useRecentActivity(
+ limit = RECENT_ACTIVITY_DISPLAY_LIMIT
+): UseRecentActivityResult {
+ const { data, isLoading, isError, error, refetch } = useQuery({
+ queryKey: queryKeys.activity.recent(limit),
+ queryFn: () => globalActivityService.getRecentActivity(limit),
+ refetchInterval: RECENT_ACTIVITY_POLL_INTERVAL_MS,
+ staleTime: 30_000, // 30 seconds
+ });
+
+ return {
+ trades: data ?? [],
+ isLoading,
+ isError,
+ error: error instanceof Error ? error : null,
+ refetch,
+ };
+}
diff --git a/src/hooks/useTrendingCreators.ts b/src/hooks/useTrendingCreators.ts
new file mode 100644
index 00000000..f60edf6c
--- /dev/null
+++ b/src/hooks/useTrendingCreators.ts
@@ -0,0 +1,33 @@
+import { useQuery } from '@tanstack/react-query';
+import { leaderboardService } from '@/services/leaderboard.service';
+import { queryKeys } from '@/lib/queryKeys';
+
+/** Refresh trending creators every 5 minutes. */
+const TRENDING_CREATORS_REFETCH_INTERVAL_MS = 5 * 60 * 1_000;
+/** Stale after 4 minutes 55 seconds so a background refetch is triggered before the 5-minute refresh. */
+const TRENDING_CREATORS_STALE_TIME_MS = (5 * 60 - 5) * 1_000;
+/** Display only the top 5 trending creators. */
+const TRENDING_CREATORS_LIMIT = 5;
+
+export interface UseTrendingCreatorsResult {
+ creators: ReturnType extends Promise ? T : never;
+ isLoading: boolean;
+ isError: boolean;
+ error: Error | null;
+}
+
+export function useTrendingCreators() {
+ const { data, isLoading, isError, error } = useQuery({
+ queryKey: queryKeys.leaderboard.volume('24h'),
+ queryFn: () => leaderboardService.getVolumeLeaderboard('24h'),
+ staleTime: TRENDING_CREATORS_STALE_TIME_MS,
+ refetchInterval: TRENDING_CREATORS_REFETCH_INTERVAL_MS,
+ });
+
+ return {
+ creators: (data ?? []).slice(0, TRENDING_CREATORS_LIMIT),
+ isLoading,
+ isError,
+ error: error instanceof Error ? error : null,
+ };
+}
diff --git a/src/lib/queryKeys.ts b/src/lib/queryKeys.ts
index 409b7fb0..16fac302 100644
--- a/src/lib/queryKeys.ts
+++ b/src/lib/queryKeys.ts
@@ -105,6 +105,10 @@ export const queryKeys = {
['leaderboard', 'volume', window] as const,
ratings: () => ['leaderboard', 'ratings'] as const,
},
+ activity: {
+ all: () => ['activity'] as const,
+ recent: (limit = 5) => ['activity', 'recent', limit] as const,
+ },
admin: {
treasury: () => ['admin', 'treasury', 'balance'] as const,
treasuryDistributions: () =>
diff --git a/src/pages/MarketplacePage.tsx b/src/pages/MarketplacePage.tsx
index 4bb6f319..f64e9357 100644
--- a/src/pages/MarketplacePage.tsx
+++ b/src/pages/MarketplacePage.tsx
@@ -15,6 +15,7 @@ import MarketplaceSection from '@/components/common/MarketplaceSection';
import SearchBar from '@/components/common/SearchBar';
import SectionDivider from '@/components/common/SectionDivider';
import StickyFilterBar from '@/components/common/StickyFilterBar';
+import TrendingCreatorsSection from '@/components/marketplace/TrendingCreatorsSection';
import { Button } from '@/components/ui/button';
import SectionErrorBoundary from '@/components/common/SectionErrorBoundary';
import {
@@ -213,6 +214,10 @@ export default function MarketplacePage() {
id="marketplace-listing-main"
aria-label="Creator key marketplace listing"
>
+
+
+
+
{
+ try {
+ const response = await this.api.get>(
+ '/activity/recent',
+ { params: { limit } }
+ );
+ return response.data.data;
+ } catch (error) {
+ throw this.handleError(error);
+ }
+ }
+}
+
+export const globalActivityService = new GlobalActivityService();