diff --git a/.github/workflows/dacs-directory-ci.yml b/.github/workflows/dacs-directory-ci.yml
index 5c1f270..844cbff 100644
--- a/.github/workflows/dacs-directory-ci.yml
+++ b/.github/workflows/dacs-directory-ci.yml
@@ -62,3 +62,9 @@ jobs:
- name: Production build
run: npm run build
+
+ - name: Install browser
+ run: npx playwright install --with-deps chromium
+
+ - name: Browser regression tests
+ run: npm run test:e2e
diff --git a/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx b/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx
index 282ca13..21e96c9 100644
--- a/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx
+++ b/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx
@@ -57,7 +57,7 @@ export default async function Deal({
const outcome = typeof raw?.["outcome"] === "string" ? raw["outcome"] : null;
return (
<>
-
diff --git a/reference-implementations/dacs-directory/app/discover/page.tsx b/reference-implementations/dacs-directory/app/discover/page.tsx
new file mode 100644
index 0000000..4c288a8
--- /dev/null
+++ b/reference-implementations/dacs-directory/app/discover/page.tsx
@@ -0,0 +1,68 @@
+import DirectoryExplorer from "@/src/components/DirectoryExplorer";
+import CatalogStatus from "@/src/components/CatalogStatus";
+import { loadCatalog } from "@/src/catalog/store";
+import { activeCatalogListings, activeCatalogSellers } from "@/src/catalog/discovery";
+import { safeJsonLd } from "@/src/components/structuredData";
+import type { Metadata } from "next";
+
+export const dynamic = "force-dynamic";
+
+export const metadata: Metadata = {
+ title: "Discover agents",
+ description: "Search verifiable agent services and inspect their signed, on-chain evidence.",
+ alternates: { canonical: "/discover" },
+};
+
+export default function Discover() {
+ const catalog = loadCatalog();
+ const sellers = activeCatalogSellers(catalog.sellers);
+ const listings = activeCatalogListings(catalog);
+ const verifiedDeals = sellers.reduce((sum, seller) => sum + seller.deals.filter((deal) => deal.refsVerified).length, 0);
+ const structuredData = {
+ "@context": "https://schema.org",
+ "@type": "ItemList",
+ name: "DACS agent services",
+ numberOfItems: listings.length,
+ itemListElement: listings.slice(0, 100).map((listing, index) => ({
+ "@type": "ListItem",
+ position: index + 1,
+ item: {
+ "@type": "Service",
+ name: listing.offering.title,
+ description: listing.offering.description,
+ identifier: `${listing.listingId}@${listing.version}`,
+ provider: { "@type": "Organization", name: listing.seller.displayName },
+ },
+ })),
+ };
+ return (
+ <>
+
+
+ verifiable agent commerce
+
+
Find agents you can verify.
+
+
+
+ Search services, compare how they get paid and deliver, then inspect the on-chain
+ evidence yourself. Humans get a clear path; agents get signed artifacts and structured discovery data.
+
+
+
{listings.length} active services
+
{sellers.length} discoverable agents
+
{verifiedDeals} strict deal bundles
+
5 DACS lifecycle stages
+
+
+
0} />
+ {catalog.generatedAt > 0 && (
+
+ Catalog indexed {new Date(catalog.generatedAt).toLocaleString()} — a cache of chain
+ state. Bundle-history hints are advisory; verification repeats signature and hash checks
+ but does not independently prove chain inclusion.
+
+ )}
+ >
+ );
+}
diff --git a/reference-implementations/dacs-directory/app/globals.css b/reference-implementations/dacs-directory/app/globals.css
index 98454c6..41d6b40 100644
--- a/reference-implementations/dacs-directory/app/globals.css
+++ b/reference-implementations/dacs-directory/app/globals.css
@@ -440,15 +440,6 @@ textarea.form-control { min-height: 112px; resize: vertical; }
.trust-strip > div:nth-child(even) { border-left: 1px solid var(--border); }
.trust-strip > div:nth-child(n + 3) { border-top: 1px solid var(--border); }
.service-layout { grid-template-columns: 1fr; }
- .nav-menu-button { display: grid; place-items: center; }
- .nav .nav-links {
- display: none; position: absolute; left: 0; right: 0; top: 100%; margin: 0; padding: 20px 24px 28px;
- flex-direction: column; align-items: stretch; gap: 4px; background: var(--bg-base); border-bottom: 1px solid var(--border);
- max-height: calc(100dvh - 70px); overflow-y: auto;
- }
- .nav .nav-links.open { display: flex; }
- .nav .nav-links a { padding: 10px 0; font-size: 1rem; color: var(--text-primary); }
- .nav .theme-toggle { margin: 10px 0 0; align-self: flex-start; }
.footer .nav-links { width: 100%; margin-left: 0 !important; gap: 12px; flex-wrap: wrap; }
}
@@ -669,9 +660,10 @@ textarea.form-control { min-height: 112px; resize: vertical; }
.tc-bubble p { font-size: .9rem; line-height: 1.5; color: var(--text-primary); }
.tc-turn-center .tc-bubble p { font-size: .82rem; color: var(--text-secondary); }
-.tc-receipt { display: inline-flex; align-items: center; gap: 8px; margin-top: 8px; padding: 5px 9px; border-radius: 8px; background: color-mix(in srgb, #0891b2 8%, transparent); border: 1px solid color-mix(in srgb, #0891b2 25%, transparent); }
-.tc-receipt-tag { font: 700 .56rem var(--font-scp, monospace); letter-spacing: .05em; color: #0e7490; }
-.tc-receipt a { font: 600 .64rem var(--font-scp, monospace); color: #0e7490; }
+.tc-receipt { --receipt-ink: #0e7490; display: inline-flex; align-items: center; gap: 8px; margin-top: 8px; padding: 5px 9px; border-radius: 8px; background: color-mix(in srgb, var(--receipt-ink) 9%, transparent); border: 1px solid color-mix(in srgb, var(--receipt-ink) 28%, transparent); }
+[data-theme="dark"] .tc-receipt { --receipt-ink: #67e8f9; }
+.tc-receipt-tag { font: 700 .56rem var(--font-scp, monospace); letter-spacing: .05em; color: var(--receipt-ink); }
+.tc-receipt a { font: 600 .64rem var(--font-scp, monospace); color: var(--receipt-ink); }
.tc-receipt code { font: 600 .64rem var(--font-scp, monospace); color: var(--text-muted); }
.tc-raw { margin-top: 8px; padding-top: 7px; border-top: 1px dashed var(--border); }
.tc-raw span { display: block; font: 700 .52rem var(--font-scp, monospace); letter-spacing: .06em; color: var(--text-muted); text-transform: uppercase; }
@@ -693,3 +685,87 @@ textarea.form-control { min-height: 112px; resize: vertical; }
.tc-stage p { display: none; }
.tc-turn { max-width: 92%; }
}
+
+/* ═══ Homepage — recorded deal landing page (scoped .hp-*, reuses .tc-* bubbles) ═ */
+.hp-page { display: flex; flex-direction: column; gap: 72px; padding-bottom: 40px; }
+.hp-hero { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 44px; align-items: center; padding-top: 16px; }
+.hp-hero-copy h1 { margin: 14px 0 14px; font-size: clamp(2rem, 4.4vw, 3.1rem); line-height: 1.06; letter-spacing: -0.02em; }
+.hp-hero-copy h1 em { font-style: italic; color: var(--accent-strong); }
+.hp-hero-copy p { color: var(--text-secondary); font-size: 1rem; line-height: 1.65; max-width: 520px; }
+.hp-cta-row { display: flex; align-items: center; gap: 18px; margin-top: 22px; flex-wrap: wrap; }
+.hp-cta-center { justify-content: center; }
+.hp-cta-ghost { color: var(--accent-strong); font-weight: 600; font-size: .92rem; }
+.hp-stats { display: flex; gap: 28px; margin-top: 28px; flex-wrap: wrap; }
+.hp-stats div { display: flex; flex-direction: column; }
+.hp-stats strong { font-size: 1.35rem; letter-spacing: -0.01em; }
+.hp-stats span { color: var(--text-muted); font-size: .72rem; }
+
+.hp-demo { border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); overflow: hidden; }
+.hp-demo-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); }
+.hp-live { display: inline-flex; align-items: center; gap: 7px; color: var(--text-muted); font: 600 .66rem var(--font-scp, monospace); letter-spacing: .04em; }
+.hp-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); animation: livePulse 1.6s infinite; }
+.hp-demo-actions { display: flex; align-items: center; gap: 10px; }
+.hp-demo-stages { display: flex; gap: 6px; }
+.hp-demo-stages span { width: 22px; height: 22px; display: grid; place-items: center; border-radius: 50%; border: 2px solid var(--border-hover); color: var(--text-muted); font: 700 .62rem var(--font-scp, monospace); transition: .25s; }
+.hp-demo-stages span.active { border-color: var(--accent); color: var(--accent-strong); }
+.hp-demo-stages span.done { border-color: var(--green-strong); background: var(--green-soft); color: var(--green-strong); }
+.hp-demo-control { border: 1px solid var(--border); border-radius: var(--radius-chip); background: var(--bg-card); color: var(--text-secondary); cursor: pointer; padding: 4px 8px; font: 600 .62rem var(--font-scp, monospace); }
+.hp-demo-control:hover { border-color: var(--border-hover); color: var(--text-primary); }
+.hp-demo-scroll { height: 380px; overflow-y: auto; padding: 16px 14px; display: flex; flex-direction: column; gap: 10px; background: var(--bg-tinted); }
+.hp-turn .tc-bubble p { font-size: .8rem; }
+.hp-turn .tc-who { font-size: .64rem; }
+.hp-turn .tc-avatar { width: 26px; height: 26px; font-size: .7rem; }
+.hp-typing { padding: 6px 10px; }
+.hp-settled { align-self: center; margin-top: 6px; color: var(--green-strong); font: 600 .68rem var(--font-scp, monospace); }
+.hp-demo-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 16px; border-top: 1px solid var(--border); font-size: .72rem; color: var(--text-muted); flex-wrap: wrap; }
+.hp-demo-foot em { font-style: normal; color: var(--text-secondary); }
+.hp-demo-foot a { color: var(--accent-strong); font-weight: 600; }
+
+.hp-section-head { max-width: 640px; margin-bottom: 24px; }
+.hp-section-head h2 { margin-top: 10px; font-size: clamp(1.4rem, 2.6vw, 1.9rem); letter-spacing: -0.015em; }
+.hp-section-head p { margin-top: 10px; color: var(--text-secondary); font-size: .92rem; line-height: 1.6; }
+
+.hp-deal-list { border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); overflow: hidden; }
+.hp-deal-row { display: grid; grid-template-columns: 200px 1fr 260px; gap: 20px; align-items: baseline; padding: 16px 20px; }
+.hp-deal-row + .hp-deal-row { border-top: 1px solid var(--border); }
+.hp-deal-name { display: flex; flex-direction: column; gap: 5px; }
+.hp-deal-name h3 { font-size: .92rem; }
+.hp-deal-row p { color: var(--text-secondary); font-size: .82rem; line-height: 1.55; }
+.hp-deal-row > code { color: var(--text-muted); font: .66rem var(--font-scp, monospace); overflow-wrap: anywhere; justify-self: end; text-align: right; }
+.hp-deal-status { align-self: flex-start; font: 600 .62rem var(--font-scp, monospace); }
+.hp-deal-status-recorded { color: var(--green-strong); }
+.hp-deal-status-live { color: var(--accent-strong); }
+.hp-deal-status-spec { color: var(--text-muted); }
+
+.hp-why-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-card); overflow: hidden; }
+.hp-why { padding: 18px 16px; }
+.hp-why + .hp-why { border-left: 1px solid var(--border); }
+.hp-why .mono { font: 600 .62rem var(--font-scp, monospace); color: var(--accent-strong); }
+.hp-why strong { display: block; margin-top: 6px; font-size: .92rem; }
+.hp-why p { margin-top: 6px; color: var(--text-secondary); font-size: .76rem; line-height: 1.55; }
+.hp-receipts-note { max-width: 640px; margin-top: 14px; color: var(--text-muted); font-size: .8rem; line-height: 1.6; }
+.hp-receipts-note a { color: var(--accent-strong); font-weight: 600; }
+
+.hp-closing { padding: 24px; border: 1px solid var(--border); border-radius: 14px; background: var(--bg-tinted); }
+.hp-closing p { margin: 8px 0 16px; max-width: 560px; color: var(--text-secondary); font-size: .875rem; }
+
+@media (max-width: 900px) {
+ .hp-hero { grid-template-columns: 1fr; }
+ .hp-deal-row { grid-template-columns: 1fr; gap: 8px; }
+ .hp-deal-row > code { justify-self: start; text-align: left; }
+ .hp-why-grid { grid-template-columns: 1fr; }
+ .hp-why + .hp-why { border-left: 0; border-top: 1px solid var(--border); }
+ .hp-demo-scroll { height: 320px; }
+}
+
+@media (max-width: 1120px) {
+ .nav-menu-button { display: grid; place-items: center; }
+ .nav .nav-links {
+ display: none; position: absolute; left: 0; right: 0; top: 100%; margin: 0; padding: 20px 24px 28px;
+ flex-direction: column; align-items: stretch; gap: 4px; background: var(--bg-base); border-bottom: 1px solid var(--border);
+ max-height: calc(100dvh - 70px); overflow-y: auto;
+ }
+ .nav .nav-links.open { display: flex; }
+ .nav .nav-links a { padding: 10px 0; font-size: 1rem; color: var(--text-primary); }
+ .nav .theme-toggle { margin: 10px 0 0; align-self: flex-start; }
+}
diff --git a/reference-implementations/dacs-directory/app/home-proposal/page.tsx b/reference-implementations/dacs-directory/app/home-proposal/page.tsx
new file mode 100644
index 0000000..d8aeb8e
--- /dev/null
+++ b/reference-implementations/dacs-directory/app/home-proposal/page.tsx
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default function HomeProposalRedirect() {
+ redirect("/");
+}
diff --git a/reference-implementations/dacs-directory/app/page.tsx b/reference-implementations/dacs-directory/app/page.tsx
index e17a597..fb994ed 100644
--- a/reference-implementations/dacs-directory/app/page.tsx
+++ b/reference-implementations/dacs-directory/app/page.tsx
@@ -1,61 +1,128 @@
-import DirectoryExplorer from "@/src/components/DirectoryExplorer";
-import CatalogStatus from "@/src/components/CatalogStatus";
+import Link from "next/link";
+import type { Metadata } from "next";
+import HomeDealDemo from "@/src/components/HomeDealDemo";
import { loadCatalog } from "@/src/catalog/store";
import { activeCatalogListings, activeCatalogSellers } from "@/src/catalog/discovery";
-import { safeJsonLd } from "@/src/components/structuredData";
export const dynamic = "force-dynamic";
+export const metadata: Metadata = {
+ title: { absolute: "DACS Directory · Verifiable agent commerce" },
+ description: "Watch a real agent-to-agent purchase with verifiable receipts, then discover the agents that trade this way.",
+ alternates: { canonical: "/" },
+};
+
+const DEAL_TYPES = [
+ {
+ name: "Fixed price",
+ line: "A posted price — 0.01 DEM per call.",
+ spec: "pricing: fixed · negotiate-fixed-price",
+ status: "live",
+ tone: "live" as const,
+ },
+ {
+ name: "Metered",
+ line: "Per-unit pricing; the total locks at commit.",
+ spec: "pricing: metered · MTR-1..5, DACS-4 v0.3",
+ status: "spec v0.3",
+ tone: "spec" as const,
+ },
+ {
+ name: "RFQ",
+ line: "Quote and counter inside a signed band. The deal above is an RFQ.",
+ spec: "pricing: negotiable · negotiate-rfq",
+ status: "recorded above",
+ tone: "recorded" as const,
+ },
+ {
+ name: "Sealed bid",
+ line: "Hidden bids, revealed together; a published rule picks the winner.",
+ spec: "pricing: auction · negotiate-sealed-envelope",
+ status: "spec v0.3",
+ tone: "spec" as const,
+ },
+];
+
export default function Home() {
const catalog = loadCatalog();
const sellers = activeCatalogSellers(catalog.sellers);
const listings = activeCatalogListings(catalog);
const verifiedDeals = sellers.reduce((sum, seller) => sum + seller.deals.filter((deal) => deal.refsVerified).length, 0);
- const structuredData = {
- "@context": "https://schema.org",
- "@type": "ItemList",
- name: "DACS agent services",
- numberOfItems: listings.length,
- itemListElement: listings.slice(0, 100).map((listing, index) => ({
- "@type": "ListItem",
- position: index + 1,
- item: {
- "@type": "Service",
- name: listing.offering.title,
- description: listing.offering.description,
- identifier: `${listing.listingId}@${listing.version}`,
- provider: { "@type": "Organization", name: listing.seller.displayName },
- },
- })),
- };
+
return (
- <>
-
-
- verifiable agent commerce
-
-
Find agents you can verify.
-
+
+
+
+
verifiable agent commerce
+
This is a real deal between two agents.
+
+ A buyer agent purchases a code audit from a seller agent — price agreed, DEM paid, work
+ delivered, five receipts on the Demos chain. This directory indexes the agents that trade this way.
+
+
+ Browse the directory
+ Run a deal yourself →
+
+
+
{listings.length} active services
+
{sellers.length} indexed agents
+
{verifiedDeals} verified deals
+
5 receipts per deal
+
-
- Search services, compare how they get paid and deliver, then inspect the on-chain
- evidence yourself. Humans get a clear path; agents get signed artifacts and structured discovery data.
-
-
-
{listings.length} active services
-
{sellers.length} discoverable agents
-
{verifiedDeals} strict deal bundles
-
5 DACS lifecycle stages
+
+
+
+
+
+
four ways to price a deal
+
From a posted price to a sealed auction
+
Every route ends in the same five receipts.
+
+
+ {DEAL_TYPES.map((deal) => (
+
+
{deal.name} {deal.status}
+
{deal.line}
+
{deal.spec}
+
+ ))}
-
0} />
- {catalog.generatedAt > 0 && (
-
- Catalog indexed {new Date(catalog.generatedAt).toLocaleString()} — a cache of chain
- state. Bundle-history hints are advisory; verification repeats signature and hash checks
- but does not independently prove chain inclusion.
+
+
+
+
one deal · five receipts
+
Why five stages
+
+
+ {[
+ { n: 1, name: "Identify", why: "One primary identity with explicitly linked wallets and Web2 accounts." },
+ { n: 2, name: "Vet", why: "Credentials, sanctions screens and reputation, checked before committing." },
+ { n: 3, name: "Negotiate", why: "Off-chain conversation, on-chain commitments. Terms anchor at commit." },
+ { n: 4, name: "Settle", why: "Value moves on the agreed rail; both sides clear in the same window." },
+ { n: 5, name: "Verify", why: "A tamper-proof attestation closes the loop. Auditable forever after." },
+ ].map((stage) => (
+
+
DACS-{stage.n}
+
{stage.name}
+
{stage.why}
+
+ ))}
+
+
+ Each stage anchors its receipt before the next begins. How it works →
- )}
- >
+
+
+
+ Run an agent? Get listed.
+ Publish a signed listing on-chain; the catalog verifies it and indexes every deal you complete.
+
+ Register an agent
+ Verify a deal yourself →
+
+
+
);
}
diff --git a/reference-implementations/dacs-directory/app/register/page.tsx b/reference-implementations/dacs-directory/app/register/page.tsx
index 3c6f5ec..0b4f070 100644
--- a/reference-implementations/dacs-directory/app/register/page.tsx
+++ b/reference-implementations/dacs-directory/app/register/page.tsx
@@ -238,7 +238,7 @@ export default function Register() {
{status && {status}
}
{publishStep === "failed" && Retry publish setScreen("review")}>Review details
}
- {screen === "done" && profileUrl && View seller profile Browse directory
}
+ {screen === "done" && profileUrl && View seller profile Browse directory
}
)}
diff --git a/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx b/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx
index 460e43a..c093ca3 100644
--- a/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx
+++ b/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx
@@ -37,7 +37,7 @@ export default async function Seller({ params }: { params: Promise<{ claim: stri
return (
<>
-
← all agents
+
← all agents
{seller.displayName}
{t.label}
diff --git a/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx b/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx
index 5afd6ab..061c13d 100644
--- a/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx
+++ b/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx
@@ -76,7 +76,7 @@ export default async function ServicePage({ params }: { params: Promise
return (
<>
- ← discover services
+ ← discover services
{listing.offering.category.replaceAll(".", " / ")}
{listing.offering.title}
diff --git a/reference-implementations/dacs-directory/app/sitemap.ts b/reference-implementations/dacs-directory/app/sitemap.ts
index dab8f7c..80fb742 100644
--- a/reference-implementations/dacs-directory/app/sitemap.ts
+++ b/reference-implementations/dacs-directory/app/sitemap.ts
@@ -10,6 +10,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
const modified = catalog.generatedAt ? new Date(catalog.generatedAt) : new Date();
const staticRoutes: MetadataRoute.Sitemap = [
{ url: base, lastModified: modified, changeFrequency: "hourly", priority: 1 },
+ { url: `${base}/discover`, lastModified: modified, changeFrequency: "hourly", priority: 0.9 },
{ url: `${base}/try`, changeFrequency: "weekly", priority: 0.9 },
{ url: `${base}/try-chat`, changeFrequency: "monthly", priority: 0.7 },
{ url: `${base}/how-it-works`, changeFrequency: "monthly", priority: 0.7 },
diff --git a/reference-implementations/dacs-directory/e2e/home.spec.ts b/reference-implementations/dacs-directory/e2e/home.spec.ts
new file mode 100644
index 0000000..3a989ee
--- /dev/null
+++ b/reference-implementations/dacs-directory/e2e/home.spec.ts
@@ -0,0 +1,32 @@
+import { expect, test } from "@playwright/test";
+
+test("the landing page leads to discovery and exposes playback controls", async ({ page }) => {
+ await page.goto("/");
+
+ await expect(page.getByRole("heading", { name: "This is a real deal between two agents." })).toBeVisible();
+ await expect(page.getByRole("link", { name: "Browse the directory" })).toHaveAttribute("href", "/discover");
+
+ const playback = page.getByRole("button", { name: "Pause" });
+ await expect(playback).toBeVisible();
+ await playback.click();
+ await expect(page.getByRole("button", { name: "Play" })).toHaveAttribute("aria-pressed", "true");
+
+ await page.goto("/discover");
+ await expect(page.getByRole("heading", { name: "Find agents you can verify." })).toBeVisible();
+});
+
+test("the proposal URL redirects to the landing page", async ({ page }) => {
+ await page.goto("/home-proposal");
+ await expect(page).toHaveURL(/\/$/);
+ await expect(page.getByRole("heading", { name: "This is a real deal between two agents." })).toBeVisible();
+});
+
+test("the primary navigation collapses before it can overflow", async ({ page }) => {
+ await page.setViewportSize({ width: 1024, height: 768 });
+ await page.goto("/");
+
+ const menu = page.getByRole("button", { name: "Open menu" });
+ await expect(menu).toBeVisible();
+ await menu.click();
+ await expect(page.getByRole("link", { name: "discover", exact: true })).toBeVisible();
+});
diff --git a/reference-implementations/dacs-directory/e2e/try-chat.spec.ts b/reference-implementations/dacs-directory/e2e/try-chat.spec.ts
index 62a3efa..a6f10b8 100644
--- a/reference-implementations/dacs-directory/e2e/try-chat.spec.ts
+++ b/reference-implementations/dacs-directory/e2e/try-chat.spec.ts
@@ -8,7 +8,7 @@ test("the recorded deal is inspectable and cannot dispatch a purchase", async ({
});
await page.goto("/try-chat");
- await expect(page.getByLabel("Recorded replay disclosure")).toContainText("It never starts a job or spends funds");
+ await expect(page.getByLabel("Recorded replay disclosure")).toContainText("never starts a job or spends funds");
await expect(page.getByRole("link", { name: /Run a live deal/ })).toHaveAttribute("href", "/try");
await page.getByRole("button", { name: /Watch the recorded deal/ }).click();
diff --git a/reference-implementations/dacs-directory/package.json b/reference-implementations/dacs-directory/package.json
index 2e4bc1d..0b6bf83 100644
--- a/reference-implementations/dacs-directory/package.json
+++ b/reference-implementations/dacs-directory/package.json
@@ -13,7 +13,7 @@
"check:deploy-config": "node scripts/check-butler-origin.mjs",
"check:butler": "node scripts/check-butler-origin.mjs --probe",
"test": "tsx --test test/*.test.ts test/*.test.mjs",
- "test:e2e": "playwright test e2e/try-dacs.spec.ts e2e/try-chat.spec.ts",
+ "test:e2e": "playwright test e2e/home.spec.ts e2e/try-dacs.spec.ts e2e/try-chat.spec.ts",
"test:e2e:live": "playwright test e2e/try-dacs.live.spec.ts",
"test:e2e:ui": "playwright test --ui",
"test:seed": "tsx --test test/seed-smoke.test.ts",
diff --git a/reference-implementations/dacs-directory/src/components/HomeDealDemo.tsx b/reference-implementations/dacs-directory/src/components/HomeDealDemo.tsx
new file mode 100644
index 0000000..c970f1d
--- /dev/null
+++ b/reference-implementations/dacs-directory/src/components/HomeDealDemo.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+import {
+ SAMPLE_PROCUREMENT_EVENTS,
+ SPEAKERS,
+ STAGES,
+ eventsToConversation,
+} from "./try-chat-script.js";
+
+const EXPLORER = "https://explorer.demos.sh";
+
+function compact(value: unknown, head = 8, tail = 6): string {
+ const text = String(value ?? "");
+ return text.length > head + tail + 1 ? `${text.slice(0, head)}…${text.slice(-tail)}` : text;
+}
+
+/**
+ * Compact, auto-playing loop of the REAL recorded purchase (the same captured
+ * run as /try-chat, every tx link genuine). Starts when scrolled into view and
+ * loops with a short hold on the settled outcome. A visible playback control
+ * lets mouse, keyboard, and touch users pause or resume it.
+ */
+export default function HomeDealDemo() {
+ const turns = useMemo(() => eventsToConversation(SAMPLE_PROCUREMENT_EVENTS), []);
+ const [visible, setVisible] = useState(0);
+ const [started, setStarted] = useState(false);
+ const [paused, setPaused] = useState(false);
+ const rootRef = useRef(null);
+ const scrollRef = useRef(null);
+
+ // Begin only when the demo is actually on screen. Users who prefer reduced
+ // motion get the finished conversation immediately — no reveal, no loop.
+ useEffect(() => {
+ const node = rootRef.current;
+ if (!node || started) return;
+ const observer = new IntersectionObserver((entries) => {
+ if (entries.some((entry) => entry.isIntersecting)) {
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
+ setVisible(turns.length);
+ setPaused(true);
+ }
+ setStarted(true);
+ }
+ }, { threshold: 0.25 });
+ observer.observe(node);
+ return () => observer.disconnect();
+ }, [started, turns.length]);
+
+ // Reveal one turn per beat; hold on the finished deal, then loop.
+ useEffect(() => {
+ if (!started || paused) return;
+ const finished = visible >= turns.length;
+ const beat = finished ? 6_000 : turns[visible]!.kind === "say" ? 850 : 1_150;
+ const timer = setTimeout(() => setVisible(finished ? 0 : visible + 1), beat);
+ return () => clearTimeout(timer);
+ }, [started, paused, visible, turns]);
+
+ // Keep the newest turn in view inside the demo's own scroll area.
+ useEffect(() => {
+ const el = scrollRef.current;
+ if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
+ }, [visible]);
+
+ const shown = turns.slice(0, visible);
+ const stage = shown.length ? shown[shown.length - 1]!.stage : 0;
+ const settled = visible >= turns.length;
+
+ return (
+
+
+
recorded deal · sec-audit via rfq
+
+
+ {STAGES.map((item, index) => (
+ index ? "done" : stage === index && shown.length ? "active" : ""} title={`${item.primitive} ${item.name}`}>
+ {settled || stage > index ? "✓" : index + 1}
+
+ ))}
+
+
setPaused((value) => !value)}
+ >
+ {paused ? "Play" : "Pause"}
+
+
+
+
+ {shown.map((turn) => {
+ const who = SPEAKERS[turn.speaker];
+ return (
+
+ {who.side !== "center" &&
{who.avatar} }
+
+ {who.side !== "center" &&
{who.name} }
+
{turn.text}
+ {(turn.txRef || turn.anchorRef) && (
+
+
{turn.kind === "pay" ? "payment" : "receipt"}
+ {turn.txRef
+ ?
tx {compact(turn.txRef)} ↗
+ :
{compact(turn.anchorRef, 10, 5)}}
+
+ )}
+
+
+ );
+ })}
+ {!settled && started &&
}
+ {settled && (
+
settled · five receipts on-chain
+ )}
+
+
+
+ );
+}
diff --git a/reference-implementations/dacs-directory/src/components/SiteNav.tsx b/reference-implementations/dacs-directory/src/components/SiteNav.tsx
index debc18c..a75d29c 100644
--- a/reference-implementations/dacs-directory/src/components/SiteNav.tsx
+++ b/reference-implementations/dacs-directory/src/components/SiteNav.tsx
@@ -5,7 +5,8 @@ import { useEffect, useRef, useState } from "react";
import ThemeToggle from "./ThemeToggle";
const LINKS = [
- { href: "/", label: "discover" },
+ { href: "/", label: "home" },
+ { href: "/discover", label: "discover" },
{ href: "/try-chat", label: "watch a deal" },
{ href: "/try", label: "try dacs", featured: true },
{ href: "/how-it-works", label: "how it works" },
diff --git a/reference-implementations/dacs-directory/src/components/TryChat.tsx b/reference-implementations/dacs-directory/src/components/TryChat.tsx
index 295fe53..ace6df3 100644
--- a/reference-implementations/dacs-directory/src/components/TryChat.tsx
+++ b/reference-implementations/dacs-directory/src/components/TryChat.tsx
@@ -59,16 +59,16 @@ export default function TryChat() {
return (
- A DACS DEAL, IN PLAIN SIGHT
- Watch two AI agents buy and sell , safely.
+ recorded deal · sec-audit via rfq
+ A recorded deal between two agents.
- On the left, the Butler — a buyer's agent. On the right, the Auditor — a
- seller's agent. They discover each other, agree terms, pay, and deliver — and every step leaves a receipt
- anyone can check on the Demos chain . No trust required.
+ The Butler (left) buys a code audit from the Auditor (right).
+ Each step anchors a receipt on the Demos chain — this is the evidence from one
+ completed purchase.
- ▶ {mode === "done" ? "Watch again" : "Watch the recorded deal"}
+ {mode === "done" ? "Watch again" : "Watch the recorded deal"}
{mode === "replay" &&
Show the full deal now }
@@ -82,7 +82,7 @@ export default function TryChat() {
Recorded RFQ replay · job d27cd332 · 20 July 2026
- This page replays evidence from one completed DEM purchase. It never starts a job or spends funds. Use Try DACS for a live procurement with recovery and duplicate-payment protection.
+ Replays one completed purchase — never starts a job or spends funds. Run one live at Try DACS.
@@ -148,7 +148,7 @@ function ChatTurn({ turn, newStage, showTech }: { turn: ConversationTurn; newSta
{turn.text}
{(turn.txRef || turn.anchorRef) && (
-
{turn.kind === "pay" ? "💰 PAYMENT" : "⛓ ON-CHAIN RECEIPT"}
+
{turn.kind === "pay" ? "payment" : "receipt"}
{turn.txRef
?
verify tx {compact(turn.txRef)} ↗
:
{compact(turn.anchorRef, 12, 6)}}
diff --git a/reference-implementations/dacs-directory/src/components/try-chat-script.ts b/reference-implementations/dacs-directory/src/components/try-chat-script.ts
index ad00a34..bfd9638 100644
--- a/reference-implementations/dacs-directory/src/components/try-chat-script.ts
+++ b/reference-implementations/dacs-directory/src/components/try-chat-script.ts
@@ -34,16 +34,17 @@ export type ConversationTurn = {
export const SPEAKERS: Record
= {
butler: { name: "Butler", role: "the buyer's agent", side: "left", avatar: "B" },
seller: { name: "Auditor", role: "the seller's agent", side: "right", avatar: "A" },
- chain: { name: "Demos chain", role: "public ledger", side: "center", avatar: "⛓" },
- referee: { name: "EvalBot", role: "independent judge", side: "center", avatar: "⚖" },
+ chain: { name: "Demos chain", role: "public ledger", side: "center", avatar: "D" },
+ referee: { name: "EvalBot", role: "independent judge", side: "center", avatar: "E" },
};
+// Stage names match demos.network: Identify · Vet · Negotiate · Settle · Verify.
export const STAGES: { name: string; primitive: string; blurb: string }[] = [
- { name: "Identify", primitive: "DACS-1", blurb: "Find a seller with a signed, on-chain listing." },
- { name: "Vet", primitive: "DACS-2", blurb: "Check who they are before committing." },
- { name: "Negotiate", primitive: "DACS-3", blurb: "Agree scope, price and timing — both sign." },
- { name: "Settle & deliver", primitive: "DACS-4", blurb: "Pay on-chain; the work arrives with evidence." },
- { name: "Verify", primitive: "DACS-5", blurb: "Bind every receipt into one checkable bundle." },
+ { name: "Identify", primitive: "DACS-1", blurb: "A signed, on-chain listing." },
+ { name: "Vet", primitive: "DACS-2", blurb: "Check the counterparty." },
+ { name: "Negotiate", primitive: "DACS-3", blurb: "Both sides sign the terms." },
+ { name: "Settle", primitive: "DACS-4", blurb: "Payment and delivery, with evidence." },
+ { name: "Verify", primitive: "DACS-5", blurb: "One bundle anyone can re-check." },
];
/** Gateway phase → DACS stage. Failures/unknowns fall back to the caller's running stage. */
@@ -69,36 +70,36 @@ export function describeEvent(event: ProcurementEvent, runningStage: StageIndex)
const stage = event.phase === "failed" ? runningStage : (PHASE_STAGE[event.phase] ?? runningStage);
// — Identify —
- if (/^Full DACS purchase queued/.test(label)) return { speaker: "butler", stage, kind: "say", text: "I need a security audit of some code — and I want the whole deal to be provable. Let me find a seller." };
- if (/Connecting the Butler buyer wallet/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Connecting my wallet and opening a secure channel…" };
- if (/Resolving the indexed Auditor'?s signed DACS-1 listing/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Looking up an auditor whose listing is signed and anchored on-chain…" };
- if (/failed current-standard verification/.test(label)) return { speaker: "butler", stage, kind: "say", text: "That listing didn't pass verification — falling back to my trusted auditor binding." };
- if (/Verified the Auditor listing/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Auditor's signed listing verified on-chain — this seller is real." };
+ if (/^Full DACS purchase queued/.test(label)) return { speaker: "butler", stage, kind: "say", text: "I need a security audit on this file, and I want the deal itself to be provable. Finding a seller." };
+ if (/Connecting the Butler buyer wallet/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Connecting my wallet and opening a private channel." };
+ if (/Resolving the indexed Auditor'?s signed DACS-1 listing/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Pulling the auditor's signed listing from the chain index." };
+ if (/failed current-standard verification/.test(label)) return { speaker: "butler", stage, kind: "say", text: "That listing failed verification. Using my configured auditor binding instead." };
+ if (/Verified the Auditor listing/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Signed listing verified. The seller's offer is on the record." };
// — Vet —
- if (/Butler scoring the verified listing/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Checking they fit my budget, capabilities and quality bar…" };
- if (/opened a signed RFQ channel/.test(label)) return { speaker: "butler", stage, kind: "say", text: "👋 Hi Auditor — I'd like a content-bound security audit. Can we agree terms?" };
- if (/Identity Vet record anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Identity check recorded on-chain — proof the vet actually happened." };
+ if (/Butler scoring the verified listing/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Scoring the offer against budget, capability and quality." };
+ if (/opened a signed RFQ channel/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Auditor: I want a content-bound audit of one file. What are your terms?" };
+ if (/Identity Vet record anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Identity vet anchored. The check itself is now on the record." };
// — Negotiate —
if (/opened|scoring/.test(label) === false && /RFQ|quote/.test(label)) return { speaker: "seller", stage, kind: "say", text: "Happy to. Let me price it against your files." };
- if (/Buyer\/seller agreement anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "The agreed terms are anchored on-chain." };
- if (/Commitment anchored before payment/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Terms locked in before a single coin moves." };
- if (/Buyer and Auditor agreed/.test(label)) { const amt = demAmount(label); return { speaker: "seller", stage, kind: "say", text: amt ? `Deal — a quick, standard audit for ${amt} DEM.` : "Deal — terms agreed." }; }
- if (/Dual-signed agreement/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Both signatures anchored — neither of us can rewrite the terms." };
+ if (/Buyer\/seller agreement anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Agreed terms anchored." };
+ if (/Commitment anchored before payment/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Commitment anchored. Terms are locked before any payment." };
+ if (/Buyer and Auditor agreed/.test(label)) { const amt = demAmount(label); return { speaker: "seller", stage, kind: "say", text: amt ? `Agreed: quick scan, standard tier, ${amt} DEM.` : "Terms agreed." }; }
+ if (/Dual-signed agreement/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Both signatures anchored. Neither side can rewrite the terms." };
// — Settle & deliver —
- if (/^Paying .*DEM to the negotiated Auditor/.test(label)) { const amt = demAmount(label); return { speaker: "butler", stage, kind: "pay", text: amt ? `Paying ${amt} DEM now, on the agreed rail.` : "Sending payment now." }; }
- if (/Payment broadcast on Demos/.test(label)) return { speaker: "chain", stage, kind: "pay", text: "Payment sent — here's the transaction, checkable by anyone." };
- if (/Auditor verified payment and is scanning/.test(label)) return { speaker: "seller", stage, kind: "say", text: "Payment received. Scanning your source now…" };
- if (/Auditor signed and anchored the content-bound report/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Signed audit report delivered and anchored — bound to the exact code." };
+ if (/^Paying .*DEM to the negotiated Auditor/.test(label)) { const amt = demAmount(label); return { speaker: "butler", stage, kind: "pay", text: amt ? `Sending ${amt} DEM on the agreed rail.` : "Sending payment." }; }
+ if (/Payment broadcast on Demos/.test(label)) return { speaker: "chain", stage, kind: "pay", text: "Payment broadcast. Transaction hash below." };
+ if (/Auditor verified payment and is scanning/.test(label)) return { speaker: "seller", stage, kind: "say", text: "Payment confirmed. Running the scan." };
+ if (/Auditor signed and anchored the content-bound report/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Signed report anchored, bound to the exact file contents." };
// — Verify —
- if (/Buyer anchoring payment evidence/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Recording my payment evidence and asking for your signed bundle…" };
- if (/Settlement evidence anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "My side of the receipt is anchored." };
- if (/Buyer attestation bundle anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "The full deal — listing, vet, terms, payment, delivery — bundled and anchored." };
- if (/EvalBot applying and signing the acceptance rubric/.test(label)) return { speaker: "referee", stage, kind: "say", text: "Independent check: does the delivery meet the agreed rubric? Signing my verdict." };
- if (/Purchase settled, report delivered/.test(label)) return { speaker: "seller", stage, kind: "say", text: "✅ Done. Paid, delivered, and the entire deal is verifiable by anyone — forever." };
+ if (/Buyer anchoring payment evidence/.test(label)) return { speaker: "butler", stage, kind: "say", text: "Anchoring my payment evidence. Requesting your bundle signature." };
+ if (/Settlement evidence anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Buyer settlement evidence anchored." };
+ if (/Buyer attestation bundle anchored/.test(label)) return { speaker: "chain", stage, kind: "anchor", text: "Full bundle anchored: listing, vet, terms, payment, delivery." };
+ if (/EvalBot applying and signing the acceptance rubric/.test(label)) return { speaker: "referee", stage, kind: "say", text: "Checking the delivery against the agreed rubric. Verdict signed." };
+ if (/Purchase settled, report delivered/.test(label)) return { speaker: "seller", stage, kind: "say", text: "Done. Paid and delivered. Anyone can re-verify this deal from the receipts." };
// Failures + anything unrecognised: honest, attributed to whoever the phase implies.
if (event.phase === "failed") return { speaker: "butler", stage, kind: "say", text: `Stopped safely: ${label}` };