+ );
+}
diff --git a/lib/copy.ts b/lib/copy.ts
index 270bec7..c302092 100644
--- a/lib/copy.ts
+++ b/lib/copy.ts
@@ -53,6 +53,18 @@ export const HERO_FIGURES = [
{ label: "gas overhead", value: "<1%" },
] as const satisfies readonly FigureCopy[];
+/** The try-it block at the foot of the hero. */
+export const TRY_HEADLINE = "try it: pull a real model.";
+
+export const TRY_LEAD =
+ "pick a model we seeded on the testnet and paste the line into a terminal on macos or linux, or into powershell on windows. it installs deCDN, opens one captcha in your browser, then pulls the model from the network and checks every byte against its blake3 hash. the testnet sponsor pays for the transfer, so there is no wallet, no token, and no sign-up.";
+
+/** The short notes under the command. */
+export const TRY_NOTES = [
+ "files land in the current directory. add -o to change it.",
+ "interrupted? run the same line again to resume.",
+] as const;
+
export const COMPARE_HEADLINE = [
"information scaled.",
"supply didn't.",
diff --git a/lib/links.ts b/lib/links.ts
index 4b97526..9d140b8 100644
--- a/lib/links.ts
+++ b/lib/links.ts
@@ -8,6 +8,9 @@ export const links = {
litepaper: "/decdn_litepaper.pdf",
presskit: "/presskit/decdn-presskit.zip",
docs: "https://docs.decdn.org/overview/introduction",
+ // Served by sponsord; each installs decdn + decdn-sponsored.
+ installer: "https://up.decdn.org/decdn.sh",
+ installerPs1: "https://up.decdn.org/decdn.ps1",
blog: "/blog/",
contact: `mailto:${EMAIL}`,
privacy: "/legal/privacy/",
diff --git a/lib/models.test.ts b/lib/models.test.ts
new file mode 100644
index 0000000..fca8ec8
--- /dev/null
+++ b/lib/models.test.ts
@@ -0,0 +1,62 @@
+import { describe, expect, it } from "vitest";
+import {
+ HASH_RE,
+ MODELS,
+ detectPlatform,
+ formatSize,
+ pullCommand,
+} from "@/lib/models";
+
+describe("launch catalogue", () => {
+ it("offers at least one model", () => {
+ expect(MODELS.length).toBeGreaterThan(0);
+ });
+
+ it("carries a well-formed b3 hash for every model", () => {
+ for (const model of MODELS) {
+ expect(model.hash, model.id).toMatch(HASH_RE);
+ }
+ });
+
+ it("has unique ids and hashes", () => {
+ expect(new Set(MODELS.map((m) => m.id)).size).toBe(MODELS.length);
+ expect(new Set(MODELS.map((m) => m.hash)).size).toBe(MODELS.length);
+ });
+});
+
+describe("pullCommand", () => {
+ it("installs and pulls by hash in one line", () => {
+ const hash = `b3:${"ab".repeat(32)}`;
+ expect(pullCommand(hash)).toBe(
+ `curl -fsSL https://up.decdn.org/decdn.sh | sh -s -- pull ${hash}`,
+ );
+ });
+
+ it("installs and pulls from PowerShell on Windows", () => {
+ const hash = `b3:${"ab".repeat(32)}`;
+ expect(pullCommand(hash, "windows")).toBe(
+ `irm https://up.decdn.org/decdn.ps1 | iex; decdn-sponsored pull ${hash}`,
+ );
+ });
+});
+
+describe("detectPlatform", () => {
+ it("picks PowerShell only for Windows", () => {
+ expect(detectPlatform("Windows")).toBe("windows");
+ expect(detectPlatform("Win32")).toBe("windows");
+ expect(
+ detectPlatform("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130"),
+ ).toBe("windows");
+ expect(detectPlatform("macOS")).toBe("unix");
+ expect(detectPlatform("MacIntel")).toBe("unix");
+ expect(detectPlatform("Linux x86_64")).toBe("unix");
+ expect(detectPlatform("Darwin")).toBe("unix");
+ });
+});
+
+describe("formatSize", () => {
+ it("renders decimal gigabytes to one place", () => {
+ expect(formatSize(28_995_469_846)).toBe("29.0 GB");
+ expect(formatSize(550_000_000)).toBe("0.6 GB");
+ });
+});
diff --git a/lib/models.ts b/lib/models.ts
new file mode 100644
index 0000000..c256f79
--- /dev/null
+++ b/lib/models.ts
@@ -0,0 +1,62 @@
+// The seeded launch catalogue, read from public/models.json at build time.
+//
+// public/models.json is written by the seeding runbook and is the single
+// source for which models the site offers: a model appears here only once its
+// verified BLAKE3 hash lands in that file. The file also ships as a static
+// asset at /models.json for anyone scripting against the catalogue.
+//
+// The command carries the hash, never the model's name: the hash is what
+// `decdn-sponsored` fetches and what every byte is verified against, so the
+// line a visitor copies names exactly the bytes they get.
+
+import catalogue from "@/public/models.json";
+import { links } from "@/lib/links";
+
+export type Model = {
+ id: string;
+ name: string;
+ repo: string;
+ license: string;
+ /** `b3:` + 64 lowercase hex characters. */
+ hash: string;
+ bytes: number;
+};
+
+export const MODELS: readonly Model[] = catalogue.models;
+
+export const HASH_RE = /^b3:[0-9a-f]{64}$/;
+
+/** The shell a visitor pastes the command into. */
+export type Platform = "unix" | "windows";
+
+export const PLATFORMS: readonly {
+ id: Platform;
+ label: string;
+ shell: string;
+ prompt: string;
+}[] = [
+ { id: "unix", label: "macos / linux", shell: "terminal", prompt: "$" },
+ { id: "windows", label: "windows", shell: "powershell", prompt: "PS>" },
+];
+
+/** The one line that installs `decdn` + `decdn-sponsored` and pulls `hash`
+ * into the current directory: a POSIX shell pipe on macOS and Linux, a
+ * PowerShell one on Windows. */
+export function pullCommand(hash: string, platform: Platform = "unix"): string {
+ return platform === "windows"
+ ? `irm ${links.installerPs1} | iex; decdn-sponsored pull ${hash}`
+ : `curl -fsSL ${links.installer} | sh -s -- pull ${hash}`;
+}
+
+/** Windows visitors get the PowerShell line; everyone else the POSIX one. */
+export function detectPlatform(platformOrUserAgent: string): Platform {
+ return /win/i.test(platformOrUserAgent) &&
+ !/darwin/i.test(platformOrUserAgent)
+ ? "windows"
+ : "unix";
+}
+
+/** Decimal gigabytes, one place: 28995469846 → "29.0 GB". */
+export function formatSize(bytes: number): string {
+ return `${(bytes / 1e9).toFixed(1)} GB`;
+}
From ce63765ee587bb2ed50d0336fd77cc487046b788 Mon Sep 17 00:00:00 2001
From: Alper Gundogdu
Date: Thu, 24 Sep 2026 16:18:11 +0100
Subject: [PATCH 2/3] fix(llms): b3: prefix in the Windows example command
Co-Authored-By: Claude Opus 5.5
---
app/llms-full.txt/route.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/llms-full.txt/route.ts b/app/llms-full.txt/route.ts
index f28bc87..967c82b 100644
--- a/app/llms-full.txt/route.ts
+++ b/app/llms-full.txt/route.ts
@@ -153,7 +153,7 @@ const compareTable = [
}),
].join("\n");
-const tryWindows = `On Windows, in PowerShell: \`${pullCommand("", "windows")}\``;
+const tryWindows = `On Windows, in PowerShell: \`${pullCommand("b3:", "windows")}\``;
const tryModels = MODELS.map(
(m) =>
From 8a5b4736f712b19265e80f16a8134fff48b4668a Mon Sep 17 00:00:00 2001
From: Alper Gundogdu
Date: Fri, 25 Sep 2026 10:59:53 +0100
Subject: [PATCH 3/3] feat(home): move try-it into its own section, make it the
hero CTA
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The try-it block becomes §04 "Try it" between How it works and the
FAQ (FAQ is now §05, Contact §06), with "try it" in the desktop and
mobile nav. The hero's primary action is "try it out" (to #try), with
the litepaper and source as the quieter links; docs stays in the nav.
llms-full.txt mirrors the block under its own "### Try it" heading.
Co-Authored-By: Claude Opus 5.5
---
app/globals.css | 4 +-
app/llms-full.txt/route.test.ts | 1 +
app/llms-full.txt/route.ts | 22 ++--
app/page.tsx | 2 +
components/site/Chrome.tsx | 10 +-
components/site/Contact.tsx | 2 +-
components/site/Faq.tsx | 4 +-
components/site/Hero.tsx | 22 ++--
components/site/MobileMenu.tsx | 3 +-
components/site/Try.tsx | 34 +++++
components/site/TryIt.tsx | 217 +++++++++++++++-----------------
lib/copy.ts | 2 +-
12 files changed, 174 insertions(+), 149 deletions(-)
create mode 100644 components/site/Try.tsx
diff --git a/app/globals.css b/app/globals.css
index b403ebd..61b1c5a 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -495,7 +495,7 @@ a.cta-primary:focus-visible {
}
}
-/* --- fleet status panel (§05) -----------------------------------------
+/* --- fleet status panel (§06) -----------------------------------------
Sits on the paper Contact section as an inverted panel. Same "tool
panel" vocabulary as .terminal — square corners, dashed border, hard
offset shadow — with an ops-dashboard layout instead of a CLI log: a
@@ -1281,7 +1281,7 @@ a.cta-primary:focus-visible {
}
}
-/* --- faq: question list + answer card (§04) ---------------------------
+/* --- faq: question list + answer card (§05) ---------------------------
One
, two columns. Questions (dt) stack on the left; every answer (dd)
occupies the whole right column and only the selected one is visible.
The selected row's ordinal turns whisper; the same number is ghosted
diff --git a/app/llms-full.txt/route.test.ts b/app/llms-full.txt/route.test.ts
index 5a8beed..408acca 100644
--- a/app/llms-full.txt/route.test.ts
+++ b/app/llms-full.txt/route.test.ts
@@ -42,6 +42,7 @@ const HOMEPAGE_SUBSECTIONS = [
"### Intro",
"### Side by side",
"### How it works",
+ "### Try it",
"### FAQ",
"### Contact",
];
diff --git a/app/llms-full.txt/route.ts b/app/llms-full.txt/route.ts
index 967c82b..c36e665 100644
--- a/app/llms-full.txt/route.ts
+++ b/app/llms-full.txt/route.ts
@@ -216,16 +216,6 @@ ${HERO_LEAD}
${figureList(HERO_FIGURES)}
-${TRY_HEADLINE}
-
-${TRY_LEAD}
-
-${tryModels}
-
-${tryWindows}
-
-${TRY_NOTES.join(" ")}
-
### Side by side
${COMPARE_HEADLINE.join(" ")}
@@ -240,6 +230,18 @@ Stack: ${STACK.join(" · ")}
${methodSteps}
+### Try it
+
+${TRY_HEADLINE}
+
+${TRY_LEAD}
+
+${tryModels}
+
+${tryWindows}
+
+${TRY_NOTES.join(" ")}
+
### FAQ
${faq}
diff --git a/app/page.tsx b/app/page.tsx
index 73be9a6..2915d6e 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -3,6 +3,7 @@ import { Contact } from "@/components/site/Contact";
import { Faq } from "@/components/site/Faq";
import { Hero } from "@/components/site/Hero";
import { Method } from "@/components/site/Method";
+import { Try } from "@/components/site/Try";
import { JsonLd } from "@/lib/jsonld";
import { faqPageNode } from "@/lib/schema";
@@ -16,6 +17,7 @@ export default function Home() {
+
diff --git a/components/site/Chrome.tsx b/components/site/Chrome.tsx
index 37ab18a..4d6a230 100644
--- a/components/site/Chrome.tsx
+++ b/components/site/Chrome.tsx
@@ -12,13 +12,21 @@ import {
import { MobileMenu } from "@/components/site/MobileMenu";
import { resolveActiveSection } from "@/components/site/chrome-active";
-const SECTION_IDS = ["intro", "compare", "method", "faq", "contact"] as const;
+const SECTION_IDS = [
+ "intro",
+ "compare",
+ "method",
+ "try",
+ "faq",
+ "contact",
+] as const;
// Hash anchors are written `/#section`. Required because this nav also
// renders on /blog/*, where a bare `#section` resolves against the
// current URL (e.g. /blog/foo/#section) instead of the home page.
const NAV = [
{ id: "compare", label: "compare" },
{ id: "method", label: "method" },
+ { id: "try", label: "try it" },
{ id: "faq", label: "faq" },
{ id: "contact", label: "contact" },
] as const;
diff --git a/components/site/Contact.tsx b/components/site/Contact.tsx
index 641e41d..ee62ed6 100644
--- a/components/site/Contact.tsx
+++ b/components/site/Contact.tsx
@@ -74,7 +74,7 @@ export function Contact() {
return (
diff --git a/components/site/Faq.tsx b/components/site/Faq.tsx
index 8fd62ef..9fe3631 100644
--- a/components/site/Faq.tsx
+++ b/components/site/Faq.tsx
@@ -3,9 +3,9 @@ import { FAQ_ITEMS } from "@/lib/faq";
import { Frame } from "@/components/ui/Frame";
import { SectionHeader } from "@/components/ui/SectionHeader";
-// Shared with the row ordinals ("04.1" …) so the numbering can't drift from
+// Shared with the row ordinals ("05.1" …) so the numbering can't drift from
// the section header.
-const SECTION = "04";
+const SECTION = "05";
export function Faq() {
return (
diff --git a/components/site/Hero.tsx b/components/site/Hero.tsx
index 1c74669..bf6ec9f 100644
--- a/components/site/Hero.tsx
+++ b/components/site/Hero.tsx
@@ -5,7 +5,6 @@ import { Figure } from "@/components/ui/Figure";
import { Frame } from "@/components/ui/Frame";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { HeroTerminal } from "@/components/site/HeroTerminal";
-import { TryIt } from "@/components/site/TryIt";
export function Hero() {
return (
@@ -32,16 +31,11 @@ export function Hero() {
{highlightBrand(HERO_LEAD)}
- {/* One primary action; docs and source sit beside it as quieter
- text links so the eye lands on the litepaper first. */}
+ {/* One primary action; the litepaper and source sit beside it as
+ quieter text links so the eye lands on the try-it section. */}
+
+ );
+}
diff --git a/components/site/TryIt.tsx b/components/site/TryIt.tsx
index 5ff9435..46c946d 100644
--- a/components/site/TryIt.tsx
+++ b/components/site/TryIt.tsx
@@ -1,9 +1,8 @@
"use client";
import { useRef, useState, useSyncExternalStore } from "react";
-import { highlightBrand } from "@/components/ui/brand";
import { Figure } from "@/components/ui/Figure";
-import { TRY_HEADLINE, TRY_LEAD, TRY_NOTES } from "@/lib/copy";
+import { TRY_NOTES } from "@/lib/copy";
import {
MODELS,
PLATFORMS,
@@ -31,9 +30,9 @@ function clientPlatform(): Platform {
const serverPlatform = (): Platform => "unix";
/**
- * The hero's closing block: pick a seeded model, copy the one line that pulls
- * it. Unlike HeroTerminal above it, this panel is real and not aria-hidden:
- * the command is the product's actual install-and-download line.
+ * The interactive half of the #try section: pick a seeded model, copy the one
+ * line that pulls it. Unlike HeroTerminal, this panel is real and not
+ * aria-hidden: the command is the product's actual install-and-download line.
*/
export function TryIt() {
const [id, setId] = useState(MODELS[0]?.id ?? "");
@@ -81,128 +80,112 @@ export function TryIt() {
return (
);
diff --git a/lib/copy.ts b/lib/copy.ts
index c302092..2536bc4 100644
--- a/lib/copy.ts
+++ b/lib/copy.ts
@@ -53,7 +53,7 @@ export const HERO_FIGURES = [
{ label: "gas overhead", value: "<1%" },
] as const satisfies readonly FigureCopy[];
-/** The try-it block at the foot of the hero. */
+/** The #try section between How it works and the FAQ. */
export const TRY_HEADLINE = "try it: pull a real model.";
export const TRY_LEAD =