diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
index dc7c2bb..8120333 100644
--- a/frontend/src/app/dashboard/page.tsx
+++ b/frontend/src/app/dashboard/page.tsx
@@ -20,6 +20,7 @@ import { useTokenMetadata } from "@/hooks/use-token-metadata";
import type { StreamData } from "@/lib/stacks";
import { STREAM_STATUS, unresolvableTokenLabel } from "@/lib/constants";
import { TopUpDialog } from "@/components/stream/top-up-dialog";
+import { NetworkStats } from "@/components/layout/network-stats";
import { Skeleton } from "@/components/ui/skeleton";
import Link from "next/link";
import { PlusCircle, Zap, Users, Coins, TrendingUp } from "lucide-react";
@@ -47,17 +48,23 @@ export default function DashboardPage() {
if (!isConnected) {
return (
- }
- title="Connect your wallet"
- description="Connect a Stacks wallet to open streams and manage real-time payments, settled on Bitcoin."
- />
+
+
+ }
+ title="Connect your wallet"
+ description="Connect a Stacks wallet to open streams and manage real-time payments, settled on Bitcoin."
+ />
+
);
}
return (
- {/* Stats row */}
+
+
+ {/* Your streams */}
+
Your streams
{isLoading ? (
Array.from({ length: 4 }).map((_, i) => (
diff --git a/frontend/src/app/organisations/page.tsx b/frontend/src/app/organisations/page.tsx
index f92ee3f..9118eff 100644
--- a/frontend/src/app/organisations/page.tsx
+++ b/frontend/src/app/organisations/page.tsx
@@ -158,15 +158,27 @@ function Hero({ directory }: { directory: OrganisationDirectory | null }) {
style={delay(2)}
>
-
- Organisations registered
-
-
- {total ?? "–"}
-
+
+
+
+ Organisations
+
+
+ {total ?? "–"}
+
+
+
+
+ Streams
+
+
+ {directory?.streamsCreated ?? "–"}
+
+
+
-
Streams tracked
+
From organisations
{streams ?? "–"}
diff --git a/frontend/src/components/landing/hero.tsx b/frontend/src/components/landing/hero.tsx
index 83d60f9..068af86 100644
--- a/frontend/src/components/landing/hero.tsx
+++ b/frontend/src/components/landing/hero.tsx
@@ -4,6 +4,7 @@ import Link from "next/link";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { LiveStreamCard } from "@/components/landing/live-stream-card";
+import { LiveStats } from "@/components/landing/live-stats";
import { ArrowRight, Zap } from "lucide-react";
export function Hero() {
@@ -81,6 +82,16 @@ export function Hero() {
+ {/* Live usage, read from the contract */}
+
+
+
+
{/* Animated streaming preview */}
+
+
+
+
+
+ {data.streamsCreated} streams on
+ mainnet
+
+ ·
+
+ {orgs}{" "}
+ {orgs === 1 ? "organisation" : "organisations"}
+
+ · read live from the contract
+
+ );
+}
diff --git a/frontend/src/components/layout/network-stats.tsx b/frontend/src/components/layout/network-stats.tsx
new file mode 100644
index 0000000..c02f395
--- /dev/null
+++ b/frontend/src/components/layout/network-stats.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import Link from "next/link";
+import { ArrowRight } from "lucide-react";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useNetworkStats } from "@/hooks/use-network-stats";
+
+/**
+ * StackStream's live usage on mainnet, shown at the top of the dashboard
+ * whether or not a wallet is connected. Both figures come from the contract.
+ */
+export function NetworkStats() {
+ const { stats, isLoading } = useNetworkStats();
+
+ if (isLoading) return ;
+ if (!stats) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ StackStream on mainnet
+
+
+ See organisations
+
+
+
+
+
+ {stats.streamsCreated}
+
+
Streams created
+
+
+
+ {stats.workspacesRegistered}
+
+
+ {stats.workspacesRegistered === 1 ? "Organisation registered" : "Organisations registered"}
+
+
+
+
+ );
+}
diff --git a/frontend/src/hooks/use-network-stats.ts b/frontend/src/hooks/use-network-stats.ts
new file mode 100644
index 0000000..8964930
--- /dev/null
+++ b/frontend/src/hooks/use-network-stats.ts
@@ -0,0 +1,33 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { OPENCLAW_API_URL } from "@/lib/constants";
+
+export interface NetworkStats {
+ /** Every stream ever opened on mainnet, by anyone (stream-manager's stream counter). */
+ streamsCreated: number;
+ /** Every organisation registered on stream-factory. */
+ workspacesRegistered: number;
+ asOf: string;
+}
+
+async function fetchNetworkStats(): Promise {
+ const res = await fetch(`${OPENCLAW_API_URL}/api/stats`);
+ if (!res.ok) throw new Error(`stats ${res.status}`);
+ return (await res.json()) as NetworkStats;
+}
+
+/**
+ * Network-wide usage, read from the contract through /api/stats. Both numbers
+ * only go up: each new stream and each new registration adds one, whoever makes
+ * it. Refreshed every minute, which is also the API's cache window.
+ */
+export function useNetworkStats() {
+ const query = useQuery({
+ queryKey: ["network-stats"],
+ queryFn: fetchNetworkStats,
+ refetchInterval: 60_000,
+ staleTime: 30_000,
+ });
+ return { stats: query.data ?? null, isLoading: query.isLoading };
+}
diff --git a/frontend/src/lib/organisations.ts b/frontend/src/lib/organisations.ts
index 1a3af10..2984ad4 100644
--- a/frontend/src/lib/organisations.ts
+++ b/frontend/src/lib/organisations.ts
@@ -15,6 +15,7 @@ import {
getDao,
getDaoCount,
getNetwork,
+ getStreamNonce,
HIRO_API_BASE,
STREAM_FACTORY_CONTRACT,
} from "./openclaw-server";
@@ -36,6 +37,12 @@ export interface OrganisationDirectory {
network: "mainnet" | "testnet";
/** `get-dao-count`, the authoritative total. */
total: number;
+ /**
+ * Every stream ever opened on mainnet (`get-stream-nonce`), by anyone, whether
+ * or not an organisation sent it. Distinct from the streams organisations
+ * have linked, which is the sum of `streamsTracked`.
+ */
+ streamsCreated: number;
/** Newest first. */
organisations: Organisation[];
/**
@@ -148,7 +155,11 @@ let cached: { directory: OrganisationDirectory; expires: number } | null = null;
export async function getOrganisationDirectory(): Promise {
if (cached && Date.now() < cached.expires) return cached.directory;
- const [rows, total] = await Promise.all([fetchFactoryTransactions(), getDaoCount()]);
+ const [rows, total, streamsCreated] = await Promise.all([
+ fetchFactoryTransactions(),
+ getDaoCount(),
+ getStreamNonce(),
+ ]);
const registrations = parseRegistrations(rows, STREAM_FACTORY_CONTRACT);
// The contract is the source of truth for each record. A registration whose
@@ -172,6 +183,7 @@ export async function getOrganisationDirectory(): Promise
const directory: OrganisationDirectory = {
network: getNetwork(),
total,
+ streamsCreated,
organisations,
unlisted: Math.max(0, total - organisations.length),
asOf: new Date().toISOString(),