+ What you get pinged about +
++ New proposals on-chain or Snapshot, vote reminders, delegation + shifts, and results for the DAOs you track. You choose the triggers; + pick a channel below to start. +
+- DAO Governance Risk Levels -
-- This platform monitors DAO governance risks and rates them through - Anticapture's Stage system. -
-- {totalMonitored} DAOs monitored by Anticapture -
-+ Governance risk, right now +
- {/* Bar Chart */} -- {item.stage} -
-- {item.riskLevel} -
-No data available
-{label}
-- {data.value}% -
-- delegated supply history -
-- {delegatedSupplyDescription} -
-+ Latest finding +
+ {/* Post titles come from the feed and run long; the design draws this + * strip as a single line on desktop, and wraps it on mobile. */} ++ {finding} +
+
+ See which DAOs could be captured,
+
+ and what it would cost an attacker.
+
+ + Live governance-security risk for every DAO we monitor, scored by + our open Stage framework, showing how exposed each DAO is to hostile + capture. +
++ Services by Blockful +
+
diff --git a/apps/dashboard/features/panel/components/TestimonialCarousel.tsx b/apps/dashboard/features/panel/components/TestimonialCarousel.tsx
new file mode 100644
index 0000000000..1d41e1dbde
--- /dev/null
+++ b/apps/dashboard/features/panel/components/TestimonialCarousel.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import Image from "next/image";
+import Link from "next/link";
+import { useState } from "react";
+
+import { TESTIMONIALS } from "@/features/panel/constants/track-record";
+import { IconButton } from "@/shared/components/design-system/buttons/icon-button/IconButton";
+
+export const TestimonialCarousel = () => {
+ const [index, setIndex] = useState(0);
+
+ const testimonial = TESTIMONIALS[index];
+ const hasMultiple = TESTIMONIALS.length > 1;
+
+ if (!testimonial) return null;
+
+ const goTo = (offset: number) =>
+ setIndex(
+ (current) =>
+ (current + offset + TESTIMONIALS.length) % TESTIMONIALS.length,
+ );
+
+ return (
+
+ goTo(-1)}
+ />
+
+ {/*
+ The arrows keep focus while the quote underneath them is swapped, so
+ without a live region a screen-reader user hears nothing and the buttons
+ read as dead. `aria-atomic` makes the quote and its author announce as
+ one testimonial rather than as two unrelated text changes.
+ */}
+
+
+
+ “{testimonial.quote}”
+
+
+
+
+
+ {testimonial.author}
+
+
+ {testimonial.handle}
+
+
+
+
+
+
+ goTo(1)}
+ />
+
+ );
+};
diff --git a/apps/dashboard/features/panel/components/TrackRecordSection.tsx b/apps/dashboard/features/panel/components/TrackRecordSection.tsx
new file mode 100644
index 0000000000..db6c6e9fed
--- /dev/null
+++ b/apps/dashboard/features/panel/components/TrackRecordSection.tsx
@@ -0,0 +1,45 @@
+import { TestimonialCarousel } from "@/features/panel/components/TestimonialCarousel";
+import { TRACK_RECORD_CASES } from "@/features/panel/constants/track-record";
+import { ClickableCard } from "@/shared/components/design-system/cards/clickable-card/ClickableCard";
+import { DaoAvatarIcon } from "@/shared/components/icons/DaoAvatarIcon";
+
+export const TrackRecordSection = () => {
+ return (
+
+
+ Track record
+
+
+
+ {TRACK_RECORD_CASES.map(
+ ({ daoId, icon: Icon, name, description, caseUrl }) => (
+
+ ) : Icon ? (
+
+ ) : (
+
+ {name.charAt(0)}
+
+ )
+ }
+ />
+ ),
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/dashboard/features/panel/components/TreasuryMonitoring.tsx b/apps/dashboard/features/panel/components/TreasuryMonitoring.tsx
deleted file mode 100644
index c8c4a0574e..0000000000
--- a/apps/dashboard/features/panel/components/TreasuryMonitoring.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-"use client";
-
-import { TooltipInfo } from "@/shared/components";
-import { InlineAlert } from "@/shared/components/design-system/alerts/inline-alert/InlineAlert";
-import { formatNumberUserReadable } from "@/shared/utils";
-
-// Fake data for now
-const monitoringData = [
- {
- title: "Treasury: Monitored vs Ecosystem total",
- tooltip:
- "Represents the funds held in DAO treasuries. The first value shows treasuries from DAOs monitored by Anticapture; the second shows the total across the ecosystem.",
- current: 4100000000, // $4.1B
- total: 16200000000, // $16.2B
- percentage: 25,
- },
-
- // Fake data for now
- {
- title: "TVL: Monitored vs Ecosystem Total",
- tooltip:
- "Represents the total value locked in DAO contracts. The first value reflects DAOs monitored by Anticapture; the second shows the ecosystem-wide total.",
- current: 5600000000, // $5.6B
- total: 35100000000, // $35.1B
- percentage: 16,
- },
-];
-
-export const TreasuryMonitoring = () => {
- return (
-
-
- {monitoringData.map((item, index) => (
-
-
-
- {item.title}
-
-
-
-
-
-
- {/* Value display */}
-
-
- {formatNumberUserReadable(item.current)}
-
-
- / {formatNumberUserReadable(item.total)}
-
-
-
- {/* Progress bar */}
-
-
-
-
-
- {item.percentage}%
-
-
-
- {index < monitoringData.length - 1 && (
-
- )}
-
- ))}
-
-
- {/* Alert */}
-
-
- );
-};
diff --git a/apps/dashboard/features/panel/components/UseItNowSection.tsx b/apps/dashboard/features/panel/components/UseItNowSection.tsx
new file mode 100644
index 0000000000..52a85c963d
--- /dev/null
+++ b/apps/dashboard/features/panel/components/UseItNowSection.tsx
@@ -0,0 +1,82 @@
+import { Bell, ChevronRight, Code2 } from "lucide-react";
+import type { ElementType } from "react";
+
+import { BadgeIcon } from "@/shared/components/design-system/badges/badge-icon/BadgeIcon";
+import { DefaultLink } from "@/shared/components/design-system/links/default-link/DefaultLink";
+
+type FeatureCard = {
+ icon: ElementType;
+ label: string;
+ description: string;
+ ctaLabel: string;
+ href: string;
+ openInNewTab: boolean;
+};
+
+const FEATURE_CARDS: FeatureCard[] = [
+ {
+ icon: Bell,
+ label: "Alerts",
+ description:
+ "Telegram or Slack pings when a DAO you track moves: new proposals on-chain or Snapshot, vote reminders, delegation shifts, results. You choose the triggers.",
+ ctaLabel: "Set up alerts",
+ href: "/alerts",
+ openInNewTab: false,
+ },
+ {
+ icon: Code2,
+ label: "API + MCP",
+ description:
+ "Give your AI agent live governance data for every DAO on this page, with a free self-service API key. Plug Anticapture into Claude, Cursor, or anything that speaks MCP.",
+ ctaLabel: "Connect your agent",
+ href: "/api-keys",
+ openInNewTab: false,
+ },
+];
+
+export const UseItNowSection = () => {
+ return (
+
+
+ Use it now
+
+
+
+ {FEATURE_CARDS.map(
+ ({ icon, label, description, ctaLabel, href, openInNewTab }) => (
+
+
+
+
+
+ {label}
+
+
+
+ {description}
+
+
+
+
+ {ctaLabel}
+
+
+
+ ),
+ )}
+
+
+ );
+};
diff --git a/apps/dashboard/features/panel/components/index.ts b/apps/dashboard/features/panel/components/index.ts
index 29a0403d94..0cfaabd67b 100644
--- a/apps/dashboard/features/panel/components/index.ts
+++ b/apps/dashboard/features/panel/components/index.ts
@@ -1,5 +1,8 @@
export * from "@/features/panel/components/PanelTable";
-export * from "@/features/panel/components/DelegatedSupplyHistory";
+export * from "@/features/panel/components/PanelHero";
+export * from "@/features/panel/components/LatestFindingTicker";
+export * from "@/features/panel/components/TrackRecordSection";
+export * from "@/features/panel/components/UseItNowSection";
+export * from "@/features/panel/components/ServicesRow";
export * from "@/features/panel/components/DaoProtectionLevels";
-export * from "@/features/panel/components/TreasuryMonitoring";
export * from "@/features/panel/components/TooltipCell";
diff --git a/apps/dashboard/features/panel/components/tooltips/DaoProtectionLevelsTooltip.tsx b/apps/dashboard/features/panel/components/tooltips/DaoProtectionLevelsTooltip.tsx
deleted file mode 100644
index ffea5350bb..0000000000
--- a/apps/dashboard/features/panel/components/tooltips/DaoProtectionLevelsTooltip.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-"use client";
-
-import type { TooltipProps } from "recharts";
-
-type StageData = {
- stage: string;
- value: number;
- riskLevel: string;
- color: string;
- description?: string;
-};
-
-export const DaoProtectionLevelsTooltip = ({
- active,
- payload,
- coordinate,
-}: TooltipProps) => {
- if (
- !active ||
- !payload?.length ||
- !coordinate ||
- coordinate.x === undefined ||
- coordinate.y === undefined
- )
- return null;
-
- const data = payload[0]?.payload as StageData;
-
- return (
-
-
-
-
- {data.stage}
-
-
-
- {data.value} DAO{data.value !== 1 ? "s" : ""}
-
-
-
- {data.description && (
-
- {data.description}
-
- )}
-
- );
-};
diff --git a/apps/dashboard/features/panel/constants/track-record.ts b/apps/dashboard/features/panel/constants/track-record.ts
new file mode 100644
index 0000000000..983604771a
--- /dev/null
+++ b/apps/dashboard/features/panel/constants/track-record.ts
@@ -0,0 +1,102 @@
+import type { ElementType } from "react";
+
+import { ArbitrumIcon } from "@/shared/components/icons/ArbitrumIcon";
+import { DaoIdEnum } from "@/shared/types/daos";
+
+export type TrackRecordCase = {
+ name: string;
+ description: string;
+ caseUrl: string;
+ /** Set when the DAO ships an icon in the dashboard. */
+ daoId?: DaoIdEnum;
+ /** Avatar for cases whose subject is not a monitored DAO (no `daoConfig` entry). */
+ icon?: ElementType;
+};
+
+export type Testimonial = {
+ quote: string;
+ /** X display name of the account the quote is lifted from. */
+ author: string;
+ /** X @handle of that account — always rendered below the name. */
+ handle: string;
+ sourceUrl: string;
+ /** Profile picture of the account the quote is lifted from. */
+ avatarSrc: string;
+};
+
+/**
+ * Case copy is final (Panel v2.1 spec); each card links to the Paragraph post
+ * that write-up lives in.
+ */
+export const TRACK_RECORD_CASES: TrackRecordCase[] = [
+ {
+ daoId: DaoIdEnum.ENS,
+ name: "ENS",
+ description:
+ "Mitigated a governance vulnerability that could have captured a $150M treasury: found and fixed before it could be exploited.",
+ caseUrl:
+ "https://paragraph.com/@blockful/a-hidden-threat-to-ens-uncovering-and-solving-a-major-governanc",
+ },
+ {
+ daoId: DaoIdEnum.UNISWAP,
+ name: "Uniswap",
+ description:
+ "Anticapture surfaced a low-cost capture path against a multi-billion-dollar treasury, quantified before it could be exploited.",
+ caseUrl:
+ "https://paragraph.com/@blockful/thanks-to-tallys-support-uniswap-dao-reached-stage-1-of-gov-se",
+ },
+ {
+ icon: ArbitrumIcon,
+ name: "Arbitrum",
+ description:
+ "Acquired 14.4M votes in a live stress test, demonstrating how cheaply governance could be swayed before the DAO hardened it.",
+ caseUrl: "https://paragraph.com/@blockful/arbitrum-security-council",
+ },
+];
+
+/**
+ * Wording, handles and links come verbatim from the X mentions audit
+ * (Growth doc 2ky4wrw9-39373, re-verified Aug 2026); each `sourceUrl` is the
+ * tweet the quote was lifted from, and each `author` is that X account's
+ * display name. Only entries the audit marks as usable social proof belong
+ * here — never paraphrase a quote or add one that is not on that doc.
+ */
+export const TESTIMONIALS: Testimonial[] = [
+ {
+ quote:
+ "using the Anticapture dashboard to break down what actually matters in practice",
+ author: "Ethereum Foundation",
+ handle: "@ethereumfndn",
+ sourceUrl: "https://x.com/ethereumfndn/status/2044080364717502737",
+ avatarSrc: "/images/testimonials/ethereumfndn.jpg",
+ },
+ {
+ quote: "makes it so easy to see when governance changes",
+ author: "Lefteris Karapetsas",
+ handle: "@LefterisJP",
+ sourceUrl: "https://x.com/LefterisJP/status/2070613219979174269",
+ avatarSrc: "/images/testimonials/lefterisjp.jpg",
+ },
+ {
+ quote:
+ "Massively impressed by the @blockful_io team's @anticapture dashboard",
+ author: "Hugo M",
+ handle: "@blockbanzai",
+ sourceUrl: "https://x.com/blockbanzai/status/1998695381194985712",
+ avatarSrc: "/images/testimonials/blockbanzai.jpg",
+ },
+ {
+ quote: "Blockful is doing the lords work keeping DAOs safe",
+ author: "Joseph Schiarizzi",
+ handle: "@CupOJoseph",
+ sourceUrl: "https://x.com/CupOJoseph/status/2034316404988449075",
+ avatarSrc: "/images/testimonials/cupojoseph.jpg",
+ },
+ {
+ quote: "@anticapture reports show ENS governance security improving",
+ author: "ENS DAO",
+ handle: "@ENS_DAO",
+ sourceUrl: "https://x.com/ENS_DAO/status/1948046880505479527",
+ avatarSrc: "/images/testimonials/ens-dao.jpg",
+ },
+];
diff --git a/apps/dashboard/public/images/testimonials/blockbanzai.jpg b/apps/dashboard/public/images/testimonials/blockbanzai.jpg
new file mode 100644
index 0000000000..0005c0c2b1
Binary files /dev/null and b/apps/dashboard/public/images/testimonials/blockbanzai.jpg differ
diff --git a/apps/dashboard/public/images/testimonials/cupojoseph.jpg b/apps/dashboard/public/images/testimonials/cupojoseph.jpg
new file mode 100644
index 0000000000..fa7b0393c0
Binary files /dev/null and b/apps/dashboard/public/images/testimonials/cupojoseph.jpg differ
diff --git a/apps/dashboard/public/images/testimonials/ens-dao.jpg b/apps/dashboard/public/images/testimonials/ens-dao.jpg
new file mode 100644
index 0000000000..d4ea5dcddd
Binary files /dev/null and b/apps/dashboard/public/images/testimonials/ens-dao.jpg differ
diff --git a/apps/dashboard/public/images/testimonials/ethereumfndn.jpg b/apps/dashboard/public/images/testimonials/ethereumfndn.jpg
new file mode 100644
index 0000000000..bebd4ac18d
Binary files /dev/null and b/apps/dashboard/public/images/testimonials/ethereumfndn.jpg differ
diff --git a/apps/dashboard/public/images/testimonials/lefterisjp.jpg b/apps/dashboard/public/images/testimonials/lefterisjp.jpg
new file mode 100644
index 0000000000..bdace41c19
Binary files /dev/null and b/apps/dashboard/public/images/testimonials/lefterisjp.jpg differ
diff --git a/apps/dashboard/shared/components/design-system/cards/clickable-card/ClickableCard.tsx b/apps/dashboard/shared/components/design-system/cards/clickable-card/ClickableCard.tsx
index 16665c37ec..fe29f0b91e 100644
--- a/apps/dashboard/shared/components/design-system/cards/clickable-card/ClickableCard.tsx
+++ b/apps/dashboard/shared/components/design-system/cards/clickable-card/ClickableCard.tsx
@@ -1,3 +1,5 @@
+import Link from "next/link";
+
import { cn } from "@/shared/utils/cn";
import { CardTitle } from "@/shared/components/design-system/cards/card-title/CardTitle";
@@ -12,42 +14,64 @@ export const ClickableCard = ({
isDisabled = false,
onClick,
className,
+ href,
+ openInNewTab = false,
}: ClickableCardProps) => {
const hasLargeDescription = Boolean(description);
+ const cardClassName = cn(
+ // Base
+ "border-border-default bg-surface-default flex w-full border p-3 text-left",
+ // Hover (suppressed when disabled via pointer-events-none)
+ "hover:bg-surface-contrast",
+ // Transition
+ "transition-colors duration-200",
+ // Disabled
+ isDisabled && "pointer-events-none cursor-not-allowed opacity-50",
+ className,
+ );
+
+ const content = hasLargeDescription ? (
+
+
+ {description}
+
+ ) : (
+
+ {avatar && {avatar}}
+
+
+ {subtitle && (
+ {subtitle}
+ )}
+
+
+ );
+
+ // A disabled card falls back to the button branch even when it has an href:
+ // an anchor stays focusable and still follows its destination on Enter, so
+ // `pointer-events-none` would only ever hide the state from pointer users.
+ if (href && !isDisabled) {
+ return (
+
+ {content}
+
+ );
+ }
+
return (
);
};
diff --git a/apps/dashboard/shared/components/design-system/cards/types.ts b/apps/dashboard/shared/components/design-system/cards/types.ts
index 41482e7c19..5aed90b2e3 100644
--- a/apps/dashboard/shared/components/design-system/cards/types.ts
+++ b/apps/dashboard/shared/components/design-system/cards/types.ts
@@ -18,4 +18,7 @@ export type ClickableCardProps = {
isDisabled?: boolean;
onClick?: () => void;
className?: string;
+ /** Renders the card as a link instead of a button. Takes precedence over onClick. */
+ href?: string;
+ openInNewTab?: boolean;
};
diff --git a/apps/dashboard/shared/components/design-system/table/Table.tsx b/apps/dashboard/shared/components/design-system/table/Table.tsx
index dc84a5c77d..74f40795de 100644
--- a/apps/dashboard/shared/components/design-system/table/Table.tsx
+++ b/apps/dashboard/shared/components/design-system/table/Table.tsx
@@ -54,6 +54,7 @@ type ColumnDef = TanstackColumnDef & {
interface DataTableProps {
className?: string;
columns: ColumnDef[];
+ containerClassName?: string;
customEmptyState?: ReactNode;
data: TData[];
disableRowClick?: (row: TData) => boolean;
@@ -92,6 +93,7 @@ interface DataTableProps {
export const Table = ({
className,
columns,
+ containerClassName,
customEmptyState,
data,
disableRowClick,
@@ -215,6 +217,7 @@ export const Table = ({
className={cn(
"text-secondary lg:bg-surface-default bg-transparent",
fillHeight && "flex h-full flex-col",
+ containerClassName,
)}
tableClassName={cn(
"border-separate border-spacing-0",
diff --git a/apps/dashboard/shared/components/icons/ArbitrumIcon.tsx b/apps/dashboard/shared/components/icons/ArbitrumIcon.tsx
new file mode 100644
index 0000000000..9f842fbdfe
--- /dev/null
+++ b/apps/dashboard/shared/components/icons/ArbitrumIcon.tsx
@@ -0,0 +1,43 @@
+import type { DaoIconProps } from "@/shared/components/icons/types";
+
+export const ArbitrumIcon = ({
+ showBackground = true,
+ ...props
+}: DaoIconProps) => {
+ return (
+
+ );
+};
diff --git a/apps/dashboard/shared/components/icons/index.ts b/apps/dashboard/shared/components/icons/index.ts
index 8f592c354e..b7eda576a7 100644
--- a/apps/dashboard/shared/components/icons/index.ts
+++ b/apps/dashboard/shared/components/icons/index.ts
@@ -1,4 +1,5 @@
export * from "@/shared/components/icons/AnticaptureIcon";
+export * from "@/shared/components/icons/ArbitrumIcon";
export * from "@/shared/components/icons/ArrowUpDown";
export * from "@/shared/components/icons/BulletPoint";
export * from "@/shared/components/icons/CookieIcon";
diff --git a/apps/dashboard/shared/services/paragraph/latestPost.ts b/apps/dashboard/shared/services/paragraph/latestPost.ts
new file mode 100644
index 0000000000..1b44e0b59d
--- /dev/null
+++ b/apps/dashboard/shared/services/paragraph/latestPost.ts
@@ -0,0 +1,83 @@
+// Reader for the blockful publication on Paragraph. Paragraph exposes no JSON
+// API for a blog's posts, so the newest publication is taken from the public
+// RSS 2.0 feed. Server-only: the feed host rejects browser origins.
+
+const FEED_URL = "https://api.paragraph.com/blogs/rss/@blockful";
+
+/** Revalidate window for the feed, in seconds. Posts ship a few times a month. */
+const REVALIDATE_SECONDS = 3600;
+
+/**
+ * Deadline for the feed request. The ticker is awaited while the homepage
+ * renders, so a Paragraph host that accepts the connection and then stalls
+ * would hold the whole page open until the transport gives up. Capped so the
+ * publication fallback is reached in about the time a reader would wait.
+ */
+const FEED_TIMEOUT_MS = 3000;
+
+export type ParagraphPost = {
+ title: string;
+ url: string;
+};
+
+/**
+ * Rendered when the feed is unreachable so the ticker never ships an empty
+ * strip. Points at the publication index, which always resolves.
+ */
+export const PARAGRAPH_PUBLICATION: ParagraphPost = {
+ title: "Research that maps DAO governance risks and capture vectors",
+ url: "https://paragraph.com/@blockful",
+};
+
+const HTML_ENTITIES: Record = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ """: '"',
+ "'": "'",
+ "'": "'",
+};
+
+const decode = (value: string) =>
+ value
+ .replace(//g, "$1")
+ .replace(
+ /&(?:amp|lt|gt|quot|apos|#39);/g,
+ (entity) => HTML_ENTITIES[entity],
+ )
+ .replace(/(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
+ .trim();
+
+const readTag = (item: string, tag: string) => {
+ const match = item.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)${tag}>`));
+ return match ? decode(match[1]) : "";
+};
+
+/**
+ * Newest post on the blockful publication. Falls back to the publication index
+ * on any transport, timeout, status or parse failure — the panel ticker must
+ * render.
+ */
+export const getLatestParagraphPost = async (): Promise => {
+ try {
+ const response = await fetch(FEED_URL, {
+ next: { revalidate: REVALIDATE_SECONDS },
+ signal: AbortSignal.timeout(FEED_TIMEOUT_MS),
+ });
+
+ if (!response.ok) return PARAGRAPH_PUBLICATION;
+
+ const feed = await response.text();
+ const [item] = feed.match(/- ]*>[\s\S]*?<\/item>/) ?? [];
+ if (!item) return PARAGRAPH_PUBLICATION;
+
+ const title = readTag(item, "title");
+ const url = readTag(item, "link") || readTag(item, "guid");
+
+ return title && url.startsWith("http")
+ ? { title, url }
+ : PARAGRAPH_PUBLICATION;
+ } catch {
+ return PARAGRAPH_PUBLICATION;
+ }
+};
+ “{testimonial.quote}” +
++ Track record +
+ +- {item.title} -
- -- {formatNumberUserReadable(item.current)} -
-- / {formatNumberUserReadable(item.total)} -
-- {item.percentage}% -
-+ Use it now +
+ ++ {label} +
++ {description} +
+{data.stage}
-- {data.value} DAO{data.value !== 1 ? "s" : ""} -
-- {data.description} -
- )} -{description}
+