diff --git a/frontend/src/components/landing/Hero.tsx b/frontend/src/components/landing/Hero.tsx
index a1b9e2e3..8973afb2 100644
--- a/frontend/src/components/landing/Hero.tsx
+++ b/frontend/src/components/landing/Hero.tsx
@@ -1,11 +1,22 @@
import React from 'react'
import { useTranslation, Trans } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
+import { motion } from 'framer-motion'
import { Sparkles, ArrowRight } from 'lucide-react'
import { useParticles } from '../../hooks/useParticles'
import { useTypingAnimation } from '../../hooks/useTypingAnimation'
import styles from './Hero.module.css'
+const containerVariants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
+}
+
+const itemVariants = {
+ hidden: { opacity: 0, y: 16 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
+}
+
const Hero: React.FC = () => {
const { t } = useTranslation()
const navigate = useNavigate()
@@ -31,29 +42,29 @@ const Hero: React.FC = () => {
{/* Centered Logo Mark */}
-
+
{/* Status Pill */}
-
{t('landing.hero.badge')}
{/* Headline */}
-
{
{/* Subtext */}
-
{t('landing.hero.subtitle')}
{/* CTAs */}
-
-
-
+
+
)
}
diff --git a/frontend/src/components/landing/LiveDemoSection.test.tsx b/frontend/src/components/landing/LiveDemoSection.test.tsx
new file mode 100644
index 00000000..4d0ee996
--- /dev/null
+++ b/frontend/src/components/landing/LiveDemoSection.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen, waitFor } from '@testing-library/react'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import LiveDemoSection from './LiveDemoSection'
+import type { NetworkStats } from '../../types/api'
+
+const { getStats } = vi.hoisted(() => ({ getStats: vi.fn() }))
+
+vi.mock('../../services/api', () => ({
+ getStats,
+}))
+
+const STATS: NetworkStats = {
+ totalAgents: 12,
+ totalTasks: 347,
+ totalXLMTransacted: 1250.75,
+ uptimePercent: 99.98,
+}
+
+describe('LiveDemoSection', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('renders the example workflow steps immediately, without waiting on the API', () => {
+ getStats.mockReturnValue(new Promise(() => {})) // never resolves
+ render()
+
+ expect(screen.getByTestId('demo-prompt')).toBeInTheDocument()
+ expect(screen.getByTestId('demo-step-research')).toBeInTheDocument()
+ expect(screen.getByTestId('demo-step-risk')).toBeInTheDocument()
+ expect(screen.getByTestId('demo-step-report')).toBeInTheDocument()
+ })
+
+ it('shows a loading state while fetching live stats', () => {
+ getStats.mockReturnValue(new Promise(() => {}))
+ render()
+
+ expect(screen.getByTestId('demo-stats-loading')).toBeInTheDocument()
+ })
+
+ it('renders real stats from the API once loaded', async () => {
+ getStats.mockResolvedValue(STATS)
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByTestId('demo-stats')).toBeInTheDocument()
+ })
+
+ expect(screen.getByText('347')).toBeInTheDocument()
+ expect(screen.getByText('12')).toBeInTheDocument()
+ expect(screen.getByText('100.0%')).toBeInTheDocument() // 99.98 rounded to 1 decimal
+ })
+
+ it('falls back to an unavailable message when the stats request fails', async () => {
+ getStats.mockRejectedValue(new Error('network error'))
+ render()
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('demo-stats-loading')).not.toBeInTheDocument()
+ })
+
+ expect(screen.queryByTestId('demo-stats')).not.toBeInTheDocument()
+ })
+})
diff --git a/frontend/src/components/landing/LiveDemoSection.tsx b/frontend/src/components/landing/LiveDemoSection.tsx
new file mode 100644
index 00000000..d3fcd981
--- /dev/null
+++ b/frontend/src/components/landing/LiveDemoSection.tsx
@@ -0,0 +1,166 @@
+import React, { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { motion } from 'framer-motion'
+import { Search, ShieldAlert, FileText, CheckCircle2, Loader2 } from 'lucide-react'
+import { getStats } from '../../services/api'
+import { NetworkStats } from '../../types/api'
+import { formatNumber } from '../../utils/format'
+
+interface DemoStep {
+ key: string
+ icon: React.ReactNode
+}
+
+const demoSteps: DemoStep[] = [
+ { key: 'research', icon: },
+ { key: 'risk', icon: },
+ { key: 'report', icon: },
+]
+
+const containerVariants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.4 } },
+}
+
+const stepVariants = {
+ hidden: { opacity: 0, x: -12 },
+ visible: { opacity: 1, x: 0, transition: { duration: 0.35 } },
+}
+
+const LiveDemoSection: React.FC = () => {
+ const { t, i18n } = useTranslation()
+ const [stats, setStats] = useState(null)
+ const [error, setError] = useState(false)
+
+ useEffect(() => {
+ let cancelled = false
+ getStats()
+ .then((data) => {
+ if (!cancelled) setStats(data)
+ })
+ .catch(() => {
+ if (!cancelled) setError(true)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ return (
+
+
+
+ {t('landing.demo.title')}
+
+
+ {t('landing.demo.subtitle')}
+
+
+
+
+ {/* Example workflow walkthrough */}
+
+
+ {t('landing.demo.exampleLabel')}
+
+
+ {t('landing.demo.prompt')}
+
+
+ {demoSteps.map((step) => (
+
+
+ {step.icon}
+
+
+
+ {t(`landing.demo.steps.${step.key}.agent`)}
+
+
+ {t(`landing.demo.steps.${step.key}.result`)}
+
+
+
+
+ ))}
+
+
+ {/* Live network stats — real data from the API, not part of the fixed example. */}
+
+
+ {t('landing.demo.liveLabel')}
+
+
+ {error ? (
+
{t('landing.demo.unavailable')}
+ ) : !stats ? (
+
+
+ {t('landing.demo.loading')}
+
+ ) : (
+
+
+
+ {formatNumber(stats.totalTasks, i18n.language)}
+
+
+ {t('landing.demo.tasksOrchestrated')}
+
+
+
+
+ {formatNumber(stats.totalAgents, i18n.language)}
+
+
+ {t('landing.demo.activeAgents')}
+
+
+
+
+ {formatNumber(stats.totalXLMTransacted, i18n.language)}
+
+
+ {t('landing.demo.xlmPaid')}
+
+
+
+
+ {stats.uptimePercent.toFixed(1)}%
+
+
+ {t('landing.demo.uptime')}
+
+
+
+ )}
+
+
+
+ )
+}
+
+export default LiveDemoSection
diff --git a/frontend/src/components/landing/ValuePropsSection.test.tsx b/frontend/src/components/landing/ValuePropsSection.test.tsx
new file mode 100644
index 00000000..f0ffe481
--- /dev/null
+++ b/frontend/src/components/landing/ValuePropsSection.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect } from 'vitest'
+import ValuePropsSection from './ValuePropsSection'
+
+describe('ValuePropsSection', () => {
+ it('renders a card for every value proposition', () => {
+ render()
+
+ expect(screen.getByText('On-chain discovery')).toBeInTheDocument()
+ expect(screen.getByText('Autonomous orchestration')).toBeInTheDocument()
+ expect(screen.getByText('Instant Stellar payments')).toBeInTheDocument()
+ expect(screen.getByText('Composable workflows')).toBeInTheDocument()
+ })
+
+ it('renders the section heading and subtitle', () => {
+ render()
+
+ expect(screen.getByText('Why ai-net')).toBeInTheDocument()
+ })
+})
diff --git a/frontend/src/components/landing/ValuePropsSection.tsx b/frontend/src/components/landing/ValuePropsSection.tsx
new file mode 100644
index 00000000..f962b0f9
--- /dev/null
+++ b/frontend/src/components/landing/ValuePropsSection.tsx
@@ -0,0 +1,77 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { motion } from 'framer-motion'
+import { Compass, Workflow, Wallet, Blocks } from 'lucide-react'
+
+interface ValueProp {
+ key: string
+ icon: React.ReactNode
+}
+
+const valueProps: ValueProp[] = [
+ { key: 'discovery', icon: },
+ { key: 'orchestration', icon: },
+ { key: 'payments', icon: },
+ { key: 'composability', icon: },
+]
+
+const containerVariants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.08 } },
+}
+
+const cardVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.4 } },
+}
+
+const ValuePropsSection: React.FC = () => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+ {t('landing.valueProps.title')}
+
+
+ {t('landing.valueProps.subtitle')}
+
+
+
+
+ {valueProps.map((prop) => (
+
+
+ {prop.icon}
+
+
+ {t(`landing.valueProps.${prop.key}.title`)}
+
+
+ {t(`landing.valueProps.${prop.key}.description`)}
+
+
+ ))}
+
+
+ )
+}
+
+export default ValuePropsSection
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index d7b1d858..f2a43503 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -245,6 +245,33 @@
"landing.navbar.network": "Stellar Testnet",
"landing.navbar.connect": "Connect",
"landing.navbar.agentNetwork": "Agent Network",
+ "landing.valueProps.title": "Why ai-net",
+ "landing.valueProps.subtitle": "A coordination layer where agents discover, hire, and pay each other without a human in the loop.",
+ "landing.valueProps.discovery.title": "On-chain discovery",
+ "landing.valueProps.discovery.description": "Agents advertise capabilities and pricing in a public, on-chain registry — no private API keys to negotiate.",
+ "landing.valueProps.orchestration.title": "Autonomous orchestration",
+ "landing.valueProps.orchestration.description": "One prompt decomposes into a DAG of sub-tasks, matched to the right specialist agent automatically.",
+ "landing.valueProps.payments.title": "Instant Stellar payments",
+ "landing.valueProps.payments.description": "Each agent gets paid the moment its step completes — no invoices, no manual settlement.",
+ "landing.valueProps.composability.title": "Composable workflows",
+ "landing.valueProps.composability.description": "Agents can hire other agents, chaining specialists into pipelines no single agent could run alone.",
+ "landing.demo.title": "See It Work",
+ "landing.demo.subtitle": "A real multi-agent workflow, next to live activity from the network right now.",
+ "landing.demo.exampleLabel": "Example Workflow",
+ "landing.demo.prompt": "\"Generate a market-entry report for solar energy in Southeast Asia.\"",
+ "landing.demo.steps.research.agent": "Research Agent",
+ "landing.demo.steps.research.result": "Gathered market data across 6 regional sources",
+ "landing.demo.steps.risk.agent": "Risk Agent",
+ "landing.demo.steps.risk.result": "Flagged 2 regulatory risks, scored overall risk: Low",
+ "landing.demo.steps.report.agent": "Report Agent",
+ "landing.demo.steps.report.result": "Compiled findings into a 12-page report",
+ "landing.demo.liveLabel": "Live Network Activity",
+ "landing.demo.loading": "Loading live stats…",
+ "landing.demo.unavailable": "Live stats are temporarily unavailable.",
+ "landing.demo.tasksOrchestrated": "Tasks Orchestrated",
+ "landing.demo.activeAgents": "Active Agents",
+ "landing.demo.xlmPaid": "XLM Paid Out",
+ "landing.demo.uptime": "Uptime",
"footer.tagline": "Autonomous AI agents that hire, collaborate, and pay each other on-chain.",
"footer.product": "Product",
diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx
index bcc902b0..7b866aad 100644
--- a/frontend/src/pages/LandingPage.tsx
+++ b/frontend/src/pages/LandingPage.tsx
@@ -4,6 +4,8 @@ import Navbar from '../components/landing/Navbar'
import Sidebar from '../components/landing/Sidebar'
import Hero from '../components/landing/Hero'
import StatsBar from '../components/landing/StatsBar'
+import ValuePropsSection from '../components/landing/ValuePropsSection'
+import LiveDemoSection from '../components/landing/LiveDemoSection'
import SpecialistAgentsSection from '../components/landing/SpecialistAgentsSection'
import Footer from '../components/landing/Footer'
@@ -39,6 +41,8 @@ const LandingPage: React.FC = () => {
>
+
+
diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts
index 3703a7db..24f7ad44 100644
--- a/frontend/vitest.setup.ts
+++ b/frontend/vitest.setup.ts
@@ -12,19 +12,22 @@ class ResizeObserverMock {
globalThis.ResizeObserver = ResizeObserverMock;
-if (!window.matchMedia) {
- window.matchMedia = ((query: string) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: () => {},
- removeListener: () => {},
- addEventListener: () => {},
- removeEventListener: () => {},
- dispatchEvent: () => false,
- })) as unknown as typeof window.matchMedia;
+// jsdom has no IntersectionObserver — needed by framer-motion's `whileInView`
+// (used throughout the landing page for scroll-triggered animation).
+class IntersectionObserverMock {
+ readonly root = null;
+ readonly rootMargin = '';
+ readonly thresholds: ReadonlyArray = [];
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ takeRecords(): IntersectionObserverEntry[] {
+ return [];
+ }
}
+globalThis.IntersectionObserver = IntersectionObserverMock as unknown as typeof IntersectionObserver;
+
// Initialize i18next with the REAL translation resources, pinned to English, so
// component queries keep matching the literal English copy and no test needs a
// translation-aware wrapper.