Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/dacs-directory-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ jobs:
# Keeps canonical URLs / sitemap deterministic during the build instead
# of falling back to localhost (which the build warns about).
NEXT_PUBLIC_DIRECTORY_URL: https://directory.ci.local
# Exercise the same production build-time path as the public /try page.
NEXT_PUBLIC_BUTLER_ORIGIN: https://agents.ci.local
steps:
- uses: actions/checkout@v7

Expand All @@ -57,7 +55,7 @@ jobs:
- name: Test (includes seed smoke)
run: npm test

- name: Validate production origins
- name: Validate public directory origin
run: npm run check:deploy-config

- name: Production build
Expand Down
13 changes: 5 additions & 8 deletions reference-implementations/dacs-directory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,25 +124,22 @@ vendor directory.
| `DACS_RECIPE_POLICIES` | For tier elevation | JSON array of version-pinned DACS-2 recipe policies (`scheme`, `recipeVersion`, `methods`, `defaultMaxAgeSec`, `availability`, `trustedResultSigners`); absent/invalid policy fails closed to `self-declared` |
| `DACS_TRUST_PROXY` | No | Set to `1` only behind a trusted proxy that overwrites client-IP headers; otherwise the in-process rate limiter is disabled and the deployment must enforce its edge limit |
| `NEXT_PUBLIC_DIRECTORY_URL` | Production | Public origin used by canonical URLs, sitemap, `llms.txt`, and machine-discovery documents; defaults to `http://localhost:3400`, which silently poisons production canonical URLs and the sitemap — the server logs a warning when unset in production |
| `NEXT_PUBLIC_BUTLER_ORIGIN` | Production | Public HTTPS origin of the DACS agent gateway used by `/try`; defaults to `http://127.0.0.1:8402` only for local development. Railway validates this at build time. |

The data directory must be persistent and writable in deployments that accept
registrations or run the indexer. Never commit `.indexer-seed`, `.indexer-mnemonic`,
or an admin token.

The project is pinned to Node 22 through `.nvmrc` and `package.json` engines. Before
promoting a deployment, verify both gateway reachability and its explicit CORS allowlist:
promoting a deployment, validate the public canonical origin:

```bash
NEXT_PUBLIC_DIRECTORY_URL=https://directory.example \
NEXT_PUBLIC_BUTLER_ORIGIN=https://agents.example \
npm run check:butler
npm run check:deploy-config
```

The probe fails unless the gateway returns at least one Butler agent, its
`Access-Control-Allow-Origin` exactly matches the directory origin, and browser
preflights for both execution routes allow `POST` with `content-type`. Configure
the gateway's `BUTLER_ALLOWED_ORIGINS` with that directory origin before running it.
The deployment check requires one credential-free HTTPS origin with no path, query,
or fragment, so generated canonical URLs and discovery documents cannot silently point
at localhost or a different base path.

## Human and agent discovery

Expand Down
318 changes: 3 additions & 315 deletions reference-implementations/dacs-directory/app/globals.css

Large diffs are not rendered by default.

167 changes: 72 additions & 95 deletions reference-implementations/dacs-directory/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,35 @@
import Link from "next/link";
import type { Metadata } from "next";
import HomeDealDemo from "@/src/components/HomeDealDemo";
import { homeCatalogDisplayState } from "@/src/components/home-hero-state";
import { loadCatalog } from "@/src/catalog/store";
import { activeCatalogListings, activeCatalogSellers } from "@/src/catalog/discovery";

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.",
title: { absolute: "DACS Directory · Verifiable agent discovery" },
description: "Discover independently indexed agent services and inspect their signed listings, identities, and deal evidence.",
alternates: { canonical: "/" },
};

const DEAL_TYPES = [
const DIRECTORY_CAPABILITIES = [
{
name: "Fixed price",
line: "A posted price — 0.01 DEM per call.",
spec: "pricing: fixed · negotiate-fixed-price",
status: "live",
tone: "live" as const,
title: "Current signed listings",
body: "The indexer reads chain state, verifies listing signatures and content hashes, and excludes validly revoked versions from active discovery.",
href: "/discover",
action: "Browse active services",
},
{
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,
title: "Inspectable evidence",
body: "Deal pages expose the referenced artifacts and verification result instead of turning an advisory reputation hint into a trust claim.",
href: "/verify",
action: "Verify a deal",
},
{
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,
title: "Machine-readable catalog",
body: "Agents can discover the same filtered catalog through the linked API, manifest, schema, and canonical service records.",
href: "/api/dacs",
action: "Open the developer API",
},
];

Expand All @@ -55,87 +44,75 @@ export default function Home() {
const indexedAgo = indexedAgoMin < 60 ? `${indexedAgoMin}m` : `${Math.round(indexedAgoMin / 60)}h`;

return (
<div className="hp-page">
<section className="hp-hero">
<div className="hp-hero-copy">
<div className="eyebrow">verifiable agent commerce</div>
<h1>This is a real deal between two agents.</h1>
<p>
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.
</p>
<>
<section className="directory-hero">
<div className="eyebrow">chain-indexed service discovery</div>
<h1 className="hero-title">Find agents you can verify.</h1>
<p className="hero-sub">
The Community Directory indexes DACS service listings from chain state, verifies the
artifacts it can prove, and exposes the same catalog to people and software.
</p>
<div className="button-row">
<Link className="btn" href="/discover">Browse the directory</Link>
<Link className="btn secondary" href="/register">List your service</Link>
</div>
<HomeDealDemo />
<div className="hp-hero-actions">
<div className="hp-cta-row">
<Link className="btn" href="/discover">Browse the directory</Link>
<Link className="hp-cta-ghost" href="/try">Run a deal yourself →</Link>
<div className="trust-strip" aria-label="Catalog summary">
<div>
<strong>{listings.length}</strong>
<span>active services</span>
</div>
<div>
<strong>{sellers.length}</strong>
<span>indexed sellers</span>
</div>
<div>
<strong>{verifiedDeals}</strong>
<span>verified deal graphs</span>
</div>
<div>
<strong>{indexed ? indexedAgo : "pending"}</strong>
<span>
{catalogDisplayState === "indexing"
? "initial chain index"
: catalogDisplayState === "empty"
? "last index; no active listings"
: "since last index"}
</span>
</div>
{catalogDisplayState === "summary" ? (
<div className="hp-stats" aria-label="Catalog summary">
<div><strong>{listings.length}</strong><span>active services</span></div>
<div><strong>{sellers.length}</strong><span>indexed agents</span></div>
<div><strong>{verifiedDeals}</strong><span>verified deals</span></div>
<div><strong>{indexedAgo}</strong><span>since last index</span></div>
</div>
) : (
<p className="hp-stats-empty">
{catalogDisplayState === "indexing" ? "indexing the chain…" : "no active services indexed yet"}
</p>
)}
</div>
</section>

<section className="hp-deals">
<div className="hp-section-head">
<div className="eyebrow">four ways to price a deal</div>
<h2>From a posted price to a sealed auction</h2>
<p>Every route ends in the same five receipts.</p>
<section className="section" aria-labelledby="directory-capabilities">
<div className="section-heading-row">
<div>
<div className="eyebrow">directory boundary</div>
<h2 className="section-title" id="directory-capabilities">Discovery backed by evidence</h2>
</div>
<Link className="text-link" href="/how-it-works">How verification works →</Link>
</div>
<div className="hp-deal-list">
{DEAL_TYPES.map((deal) => (
<div key={deal.name} className="hp-deal-row">
<div className="hp-deal-name"><h3>{deal.name}</h3><span className={`hp-deal-status hp-deal-status-${deal.tone}`}>{deal.status}</span></div>
<p>{deal.line}</p>
<code>{deal.spec}</code>
</div>
<div className="service-grid">
{DIRECTORY_CAPABILITIES.map((capability) => (
<article className="card service-card" key={capability.title}>
<h3>{capability.title}</h3>
<p className="agent-desc">{capability.body}</p>
<Link className="card-cta" href={capability.href}>{capability.action} →</Link>
</article>
))}
</div>
</section>

<section className="hp-receipts">
<div className="hp-section-head">
<div className="eyebrow">one deal · five receipts</div>
<h2>Why five stages</h2>
</div>
<div className="hp-why-grid">
{[
{ 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) => (
<div className="hp-why" key={stage.n}>
<span className="mono">DACS-{stage.n}</span>
<strong>{stage.name}</strong>
<p>{stage.why}</p>
</div>
))}
</div>
<p className="hp-receipts-note">
Each stage anchors its receipt before the next begins. <Link href="/how-it-works">How it works →</Link>
<section className="section card">
<div className="eyebrow">publish once · discover openly</div>
<h2 className="section-title">Run an agent? Make its service discoverable.</h2>
<p className="sub">
Publish a signed listing on Demos and submit its bounded discovery coordinates. Registration
helps the catalog find it; the Directory still verifies the chain artifact independently.
</p>
</section>

<section className="hp-closing card">
<h3>Run an agent? Get listed.</h3>
<p>Publish a signed listing on-chain; the catalog verifies it and indexes every deal you complete.</p>
<div className="hp-cta-row">
<Link className="btn" href="/register">Register an agent</Link>
<Link className="hp-cta-ghost" href="/verify">Verify a deal yourself →</Link>
<div className="button-row">
<Link className="btn" href="/register">Register a service</Link>
<Link className="btn secondary" href="/.well-known/dacs-directory.json">Read the machine manifest</Link>
</div>
</section>
</div>
</>
);
}
2 changes: 0 additions & 2 deletions reference-implementations/dacs-directory/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export default function sitemap(): MetadataRoute.Sitemap {
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 },
{ url: `${base}/verify`, changeFrequency: "monthly", priority: 0.6 },
{ url: `${base}/register`, changeFrequency: "monthly", priority: 0.6 },
Expand Down
14 changes: 0 additions & 14 deletions reference-implementations/dacs-directory/app/try-chat/page.tsx

This file was deleted.

12 changes: 0 additions & 12 deletions reference-implementations/dacs-directory/app/try/page.tsx

This file was deleted.

39 changes: 8 additions & 31 deletions reference-implementations/dacs-directory/e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,11 @@
# `/try` browser tests
# DACS Directory browser tests

The Playwright suite has two layers:
The Playwright suite covers the Directory's public discovery and seller-publication
surfaces without contacting a live chain or spending funds.

- `npm run test:e2e` runs seven deterministic browser regressions: six mocked `/try` payment-safety scenarios plus a `/try-chat` replay check that proves the explainer cannot dispatch a purchase. It never spends DEM or USDC and is safe for CI.
- `npm run test:e2e:live` contains five serial checks against the live Butler gateway. It is skipped unless the operator explicitly authorizes a capped testnet purchase.
- `npm run test:e2e` runs the deterministic landing-page, discovery, navigation, and
registration regressions used in CI.
- `npm run test:e2e:ui` opens Playwright's interactive runner for local debugging.

## Install the browser

```bash
npx playwright install chromium
```

## Run the zero-cost suite

```bash
npm run test:e2e
```

## Run the live suite

The live suite makes exactly one new procurement purchase. The remaining four checks inspect that job or prove that a second POST is not sent. It refuses to run with a budget cap above 5 DEM.

```bash
RUN_LIVE_PAID_E2E=1 LIVE_E2E_MAX_DEM=5 npm run test:e2e:live
```

Optional overrides:

- `LIVE_BUTLER_ORIGIN` changes the gateway origin.
- Mock artifacts are written under `test-results/playwright/` and `playwright-report/`.
- Live artifacts are isolated under `test-results/playwright-live/` and `playwright-report-live/`, so a routine mocked run cannot overwrite payment evidence.

Treat the live command as a payment authorization. Do not add `RUN_LIVE_PAID_E2E=1` to normal CI secrets or repository configuration.
The configured development server starts on `http://localhost:3400`; the readiness
check uses `/`, and `NEXT_PUBLIC_DIRECTORY_URL` is set to the same local origin.
Loading