- 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.
-
+ <>
+
+
chain-indexed service discovery
+
Find agents you can verify.
+
+ 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.
+
- {[
- { 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 →
+
+
publish once · discover openly
+
Run an agent? Make its service discoverable.
+
+ 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.
-
-
-
-
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 →
+
+ Register a service
+ Read the machine manifest
-
+ >
);
}
diff --git a/reference-implementations/dacs-directory/app/sitemap.ts b/reference-implementations/dacs-directory/app/sitemap.ts
index 80fb742..93ba8fa 100644
--- a/reference-implementations/dacs-directory/app/sitemap.ts
+++ b/reference-implementations/dacs-directory/app/sitemap.ts
@@ -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 },
diff --git a/reference-implementations/dacs-directory/app/try-chat/page.tsx b/reference-implementations/dacs-directory/app/try-chat/page.tsx
deleted file mode 100644
index 4b63c87..0000000
--- a/reference-implementations/dacs-directory/app/try-chat/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import TryChat from "@/src/components/TryChat";
-import type { Metadata } from "next";
-
-export const metadata: Metadata = {
- title: "Watch a recorded DACS deal",
- description: "Replay a completed deal between a buyer's Butler and a seller's Auditor, with genuine on-chain receipts and no new payment.",
- alternates: { canonical: "/try-chat" },
-};
-
-// Zero-cost explainer. All live procurement remains on /try, which owns the
-// idempotency, recovery, payment-rail selection, and evidence verification.
-export default function TryChatPage() {
- return ;
-}
diff --git a/reference-implementations/dacs-directory/app/try/page.tsx b/reference-implementations/dacs-directory/app/try/page.tsx
deleted file mode 100644
index 8194fb1..0000000
--- a/reference-implementations/dacs-directory/app/try/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import TryDacs from "@/src/components/TryDacs";
-import type { Metadata } from "next";
-
-export const metadata: Metadata = {
- title: "Try DACS",
- description: "Run a live fixed-price or RFQ agent procurement, pay with DEM or Base Sepolia USDC through x402, and inspect every DACS receipt.",
- alternates: { canonical: "/try" },
-};
-
-export default function TryPage() {
- return ;
-}
diff --git a/reference-implementations/dacs-directory/e2e/README.md b/reference-implementations/dacs-directory/e2e/README.md
index e0dca01..be70db5 100644
--- a/reference-implementations/dacs-directory/e2e/README.md
+++ b/reference-implementations/dacs-directory/e2e/README.md
@@ -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.
diff --git a/reference-implementations/dacs-directory/e2e/home.spec.ts b/reference-implementations/dacs-directory/e2e/home.spec.ts
index b3502c2..9c69676 100644
--- a/reference-implementations/dacs-directory/e2e/home.spec.ts
+++ b/reference-implementations/dacs-directory/e2e/home.spec.ts
@@ -1,24 +1,19 @@
import { expect, test } from "@playwright/test";
-test("the landing page leads to discovery and exposes playback controls", async ({ page }) => {
+test("the landing page is catalog-led", 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();
+ await expect(page.getByRole("link", { name: "Browse the directory" })).toHaveAttribute("href", "/discover");
+ await expect(page.getByRole("link", { name: "List your service", exact: true })).toHaveAttribute("href", "/register");
+ await expect(page.getByLabel("Catalog summary")).toBeVisible();
+ await expect(page.getByText("initial chain index")).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();
+ await expect(page.getByRole("heading", { name: "Find agents you can verify." })).toBeVisible();
});
test("the primary navigation collapses before it can overflow", async ({ page }) => {
@@ -29,44 +24,18 @@ test("the primary navigation collapses before it can overflow", async ({ page })
await expect(menu).toBeVisible();
await menu.click();
await expect(page.getByRole("link", { name: "discover", exact: true })).toBeVisible();
+ await expect(page.getByRole("link", { name: "list your service", exact: true })).toBeVisible();
});
-test("the mobile hero keeps its visual and keyboard order aligned", async ({ page }) => {
+test("the mobile landing page keeps primary actions and catalog state visible", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
- const heroBlocks = page.locator(".hp-hero-copy, .hp-demo, .hp-hero-actions");
- await expect(heroBlocks).toHaveCount(3);
- const blockTops = await heroBlocks.evaluateAll((blocks) =>
- blocks.map((block) => Math.round(block.getBoundingClientRect().top)),
- );
- assertNondecreasing(blockTops);
-
- const emptyState = page.locator(".hp-stats-empty");
- await expect(emptyState).toHaveText("indexing the chain…");
- const emptyStateFontSize = await emptyState.evaluate((element) =>
- Number.parseFloat(getComputedStyle(element).fontSize),
- );
- expect(emptyStateFontSize).toBeLessThan(13);
-
- const focusableLabels = await page.locator(".hp-hero a, .hp-hero button").evaluateAll((elements) =>
- elements.map((element) => element.textContent?.trim()),
- );
- expect(focusableLabels).toEqual([
- "Pause",
- "try dacs →",
- "Browse the directory",
- "Run a deal yourself →",
- ]);
+ await expect(page.getByRole("heading", { name: "Find agents you can verify." })).toBeVisible();
+ await expect(page.getByRole("link", { name: "Browse the directory" })).toBeVisible();
+ await expect(page.getByRole("link", { name: "List your service", exact: true })).toBeVisible();
- const focusableTops = await page.locator(".hp-hero a, .hp-hero button").evaluateAll((elements) =>
- elements.map((element) => Math.round(element.getBoundingClientRect().top)),
- );
- assertNondecreasing(focusableTops);
+ const summary = page.getByLabel("Catalog summary");
+ await expect(summary).toBeVisible();
+ await expect(summary.locator(":scope > div")).toHaveCount(4);
});
-
-function assertNondecreasing(values: number[]) {
- for (let index = 1; index < values.length; index += 1) {
- expect(values[index]).toBeGreaterThanOrEqual(values[index - 1]!);
- }
-}
diff --git a/reference-implementations/dacs-directory/e2e/try-chat.spec.ts b/reference-implementations/dacs-directory/e2e/try-chat.spec.ts
deleted file mode 100644
index 6467998..0000000
--- a/reference-implementations/dacs-directory/e2e/try-chat.spec.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { expect, test } from "@playwright/test";
-
-test("the recorded deal is inspectable and cannot dispatch a purchase", async ({ page }) => {
- let procurementRequests = 0;
- await page.route("**/demo/procurement**", async (route) => {
- procurementRequests += 1;
- await route.abort("blockedbyclient");
- });
-
- await page.goto("/try-chat");
- 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();
- await page.getByRole("button", { name: "Show the full deal now" }).click();
-
- await expect(page.locator(".tc-outcome-badge")).toContainText("Recorded deal settled & verified");
- await expect(page.locator(".tc-stage-done")).toHaveCount(5);
- const payment = page.getByRole("link", { name: /verify tx 53dd8a7b…e0ff24/ });
- await expect(payment).toHaveAttribute("href", "https://explorer.demos.sh/transactions/53dd8a7b34f7d29377c27599e17a5742b2c7296dd048b1235c04359957e0ff24");
- expect(procurementRequests).toBe(0);
-});
diff --git a/reference-implementations/dacs-directory/e2e/try-dacs-fixtures.ts b/reference-implementations/dacs-directory/e2e/try-dacs-fixtures.ts
deleted file mode 100644
index e80b787..0000000
--- a/reference-implementations/dacs-directory/e2e/try-dacs-fixtures.ts
+++ /dev/null
@@ -1,282 +0,0 @@
-import { expect, type BrowserContext, type Page, type Route } from "@playwright/test";
-
-export const PROCUREMENT_RUN_KEY = "dacs-try:procurement-run";
-export const PROCUREMENT_LOCK_NAME = "dacs-try:procurement-dispatch";
-
-export const exampleInput = {
- goal: "Audit the supplied source and return a content-bound security report.",
- budgetDem: 5,
- files: [{ path: "app.js", content: "export const greeting = 'hello';\n" }],
-};
-
-const x402Governance = {
- status: "operator-provisional",
- conformantAuthority: false,
- signer: "did:demos:agent:mock-steward",
- disclosure: "https://github.com/DACS-Agent-commerce/DACS-Standard/issues/274",
-};
-
-function railReadiness() {
- return {
- "pay-dem": { executable: true, reasons: [] },
- "pay-x402": { executable: true, reasons: [], railGovernance: x402Governance },
- };
-}
-
-const commonProfile = {
- timing: { healthyMinSec: 10, healthyMaxSec: 30, hardTimeoutSec: 180, protocolFloorSec: 0 },
- confirmationGates: ["commit-agreement", "payment"],
- paymentRails: ["pay-dem", "pay-x402"],
- implementationStatus: "live",
- executable: true,
- reasons: [],
-};
-
-export const procurementOptions = {
- profiles: [
- {
- ...commonProfile,
- id: "oracle-auto-accept",
- title: "Buy an attested crypto price",
- agentName: "Oracle Desk",
- serviceId: "oracle-data",
- mode: "fixed-price-auto-accept",
- negotiationPhase: "negotiate-fixed-price",
- summary: "Buy a posted-price public data point.",
- fields: [],
- sampleInput: { product: "crypto-price", params: { id: "bitcoin" }, paymentRail: "pay-dem" },
- railInputs: [
- { rail: "pay-dem", fields: [], sampleInput: { product: "crypto-price", params: { id: "bitcoin" }, paymentRail: "pay-dem" } },
- { rail: "pay-x402", fields: [], sampleInput: { product: "crypto-price", params: { id: "bitcoin" }, paymentRail: "pay-x402" } },
- ],
- railReadiness: railReadiness(),
- },
- {
- ...commonProfile,
- id: "dd-live-fixed",
- title: "Commission a due-diligence report",
- agentName: "Due Diligence Researcher",
- serviceId: "due-diligence",
- mode: "fixed-price-co-sign",
- negotiationPhase: "negotiate-fixed-price",
- summary: "Buy a jointly signed fixed-price research report.",
- fields: [],
- sampleInput: { kind: "npm-package", subject: "express", paymentRail: "pay-dem" },
- railInputs: [
- { rail: "pay-dem", fields: [], sampleInput: { kind: "npm-package", subject: "express", paymentRail: "pay-dem" } },
- { rail: "pay-x402", fields: [], sampleInput: { kind: "npm-package", subject: "express", paymentRail: "pay-x402" } },
- ],
- railReadiness: railReadiness(),
- },
- {
- ...commonProfile,
- id: "security-audit-rfq",
- title: "Negotiate a bounded security audit",
- agentName: "Security Auditor",
- serviceId: "security-audit",
- mode: "rfq",
- negotiationPhase: "negotiate-rfq",
- summary: "Run a live RFQ and buy a content-bound security report.",
- fields: [],
- sampleInput: { ...exampleInput, paymentRail: "pay-dem" },
- railInputs: [
- { rail: "pay-dem", fields: [], sampleInput: { ...exampleInput, paymentRail: "pay-dem" } },
- {
- rail: "pay-x402",
- fields: [],
- sampleInput: { goal: exampleInput.goal, budgetUsdc: 0.1, files: exampleInput.files, paymentRail: "pay-x402" },
- },
- ],
- railReadiness: railReadiness(),
- },
- ],
-};
-
-const at = "2026-07-20T12:00:00.000Z";
-
-export const acceptedResult = {
- status: "settled-and-accepted",
- decision: {
- outcome: "selected",
- winner: { provider: "Security Auditor", listingId: "audit-negotiator", price: 1 },
- candidates: [{ provider: "Security Auditor", listingId: "audit-negotiator", askPrice: 1, chosenRail: "pay-dem" }],
- },
- negotiation: {
- protocol: "l2ps",
- terms: { tier: "bounded", deadline: "5 minutes", price: 1 },
- buyerSignature: { party: "buyer", algorithm: "ed25519", value: "buyer-signature" },
- sellerSignature: { party: "seller", algorithm: "ed25519", value: "seller-signature" },
- },
- settlement: {
- amountDem: 1,
- rail: "pay-dem",
- payer: "did:demos:buyer",
- payee: "did:demos:seller",
- txHash: "mock-payment-transaction",
- },
- delivery: { verified: true, report: { findings: [] } },
- evaluation: {
- accepted: true,
- rulingValid: true,
- ruling: { verdict: "accept" },
- },
- bundleVerification: { ok: true },
- reconciliation: { reconciled: true },
- anchors: { listing: "mock-listing-anchor", agreement: "mock-agreement-anchor" },
- transactions: [
- { kind: "listing", name: "DACS-1 listing", address: "mock-listing-anchor", txRef: "mock-listing-tx" },
- { kind: "vet", name: "DACS-2 vet", address: "mock-vet-anchor", txRef: "mock-vet-tx" },
- { kind: "agreement", name: "DACS-3 agreement", address: "mock-agreement-anchor", txRef: "mock-agreement-tx" },
- { kind: "payment", name: "DEM payment", txRef: "mock-payment-transaction" },
- { kind: "bundle", name: "DACS-5 bundle", address: "mock-bundle-anchor", txRef: "mock-bundle-tx" },
- ],
-};
-
-export const completedJob = {
- id: "job-e2e-1",
- status: "complete",
- phase: "complete",
- preview: null,
- events: [
- { phase: "discovering", label: "Signed listing verified", at, txRef: "mock-listing-tx" },
- { phase: "selecting", label: "Counterparty vet anchored", at, txRef: "mock-vet-tx" },
- { phase: "agreeing", label: "Dual-signed agreement anchored", at, txRef: "mock-agreement-tx" },
- { phase: "settling", label: "Payment evidence recorded", at, txRef: "mock-payment-transaction" },
- { phase: "complete", label: "Reconciled DACS-5 bundle anchored", at, txRef: "mock-bundle-tx" },
- ],
- result: acceptedResult,
-};
-
-export const securityPreviewJob = {
- id: completedJob.id,
- status: "running",
- phase: "verifying",
- events: [
- ...completedJob.events.slice(0, 4),
- { phase: "verifying", label: "Seller delivery and payment evidence verified", at, txRef: "mock-delivery-tx" },
- ],
- preview: {
- kind: "dacs-procurement-delivery-preview",
- status: "report-verified-finalising-dacs5",
- jobId: "web-auditor-e2e-1",
- delivery: {
- verified: true,
- report: {
- version: 1,
- target: "(posted content)",
- findings: [{
- id: "SEC-1",
- severity: "high",
- title: "Unsafe dynamic execution",
- detail: "A dynamic code path requires review.",
- file: "app.js",
- line: 4,
- }],
- },
- },
- anchors: {
- listing: "mock-listing-anchor",
- agreement: "mock-agreement-anchor",
- commitment: "mock-commitment-anchor",
- paymentEvidence: "mock-payment-evidence-anchor",
- delivery: "mock-delivery-anchor",
- deliveryEvidence: "mock-delivery-evidence-anchor",
- },
- },
-};
-
-export const failedSecurityPreviewJob = {
- ...securityPreviewJob,
- status: "failed",
- phase: "verifying",
- error: "Buyer DACS-5 bundle anchoring failed after payment evidence was recorded.",
-};
-
-const x402PaymentTx = `0x${"ab".repeat(32)}`;
-
-export const x402CompletedJob = {
- ...completedJob,
- events: completedJob.events.map((event) => event.phase === "settling"
- ? { ...event, txRef: x402PaymentTx }
- : event),
- result: {
- ...acceptedResult,
- decision: {
- ...acceptedResult.decision,
- winner: { ...acceptedResult.decision.winner, price: 0.05 },
- candidates: acceptedResult.decision.candidates.map((candidate) => ({
- ...candidate,
- askPrice: 0.05,
- chosenRail: "pay-x402",
- })),
- },
- negotiation: {
- ...acceptedResult.negotiation,
- terms: { ...acceptedResult.negotiation.terms, price: { amount: 0.05, currency: "USDC" } },
- },
- settlement: {
- amount: { amount: 0.05, currency: "USDC" },
- rail: "pay-x402",
- payer: "0x1111111111111111111111111111111111111111",
- payee: "0x2222222222222222222222222222222222222222",
- txHash: x402PaymentTx,
- railGovernance: x402Governance,
- },
- transactions: acceptedResult.transactions.map((transaction) => transaction.kind === "payment"
- ? { ...transaction, name: "x402 USDC payment", txRef: x402PaymentTx }
- : transaction),
- },
-};
-
-export type MockGatewayOptions = {
- onProcurementPost?: (route: Route) => Promise | void;
- onProcurementGet?: (route: Route) => Promise | void;
-};
-
-function json(route: Route, body: unknown, status = 200) {
- return route.fulfill({
- status,
- contentType: "application/json",
- body: JSON.stringify(body),
- headers: { "access-control-allow-origin": "*" },
- });
-}
-
-export async function installMockGateway(context: BrowserContext, options: MockGatewayOptions = {}) {
- await context.route("**/api/dacs/listings?**", (route) => json(route, { listings: [] }));
- // Register the wildcard first: Playwright evaluates matching routes in
- // reverse registration order, so the explicit /options contract below wins.
- await context.route("**/demo/procurement/*", async (route) => {
- if (options.onProcurementGet) return options.onProcurementGet(route);
- return json(route, completedJob);
- });
- await context.route("**/demo/procurement", async (route) => {
- if (route.request().method() === "OPTIONS") return json(route, {});
- if (options.onProcurementPost) return options.onProcurementPost(route);
- return json(route, completedJob);
- });
- await context.route("**/demo/procurement/options", (route) => json(route, procurementOptions));
-}
-
-export async function chooseProcurementExample(page: Page, rail: "pay-dem" | "pay-x402" = "pay-dem") {
- await page.goto("/try");
- const agent = page.getByRole("button", { name: /Security Auditor/ }).first();
- await expect(agent).toBeVisible();
- await agent.click();
- const railName = rail === "pay-x402" ? /USDC · x402/ : /DEM · Demos/;
- const railButton = page.getByRole("group", { name: "Payment rail" }).getByRole("button", { name: railName });
- await expect(railButton).toBeEnabled();
- await railButton.click();
- await page.getByRole("button", { name: "Load example" }).click();
- await expect(page.getByRole("button", { name: /Run the full deal/ })).toBeEnabled();
-}
-
-export async function expectAcceptedEvidence(page: Page) {
- await expect(page.getByRole("heading", { name: "Security Auditor result" })).toBeVisible();
- await expect(page.getByText("Settled & accepted", { exact: true })).toBeVisible();
- await expect(page.getByText("broadcast & recorded", { exact: true })).toBeVisible();
- await expect(page.getByText("dual-signed", { exact: true })).toBeVisible();
- await expect(page.getByText("accepted", { exact: true }).last()).toBeVisible();
- await expect(page.locator(".journey-step.complete")).toHaveCount(5);
- await expect(page.getByRole("link", { name: /View on explorer/ })).toBeVisible();
-}
diff --git a/reference-implementations/dacs-directory/e2e/try-dacs.live.spec.ts b/reference-implementations/dacs-directory/e2e/try-dacs.live.spec.ts
deleted file mode 100644
index 371215c..0000000
--- a/reference-implementations/dacs-directory/e2e/try-dacs.live.spec.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-import { expect, test, type BrowserContext, type Page, type Request } from "@playwright/test";
-import {
- PROCUREMENT_LOCK_NAME,
- PROCUREMENT_RUN_KEY,
- chooseProcurementExample,
- expectAcceptedEvidence,
-} from "./try-dacs-fixtures.js";
-
-const LIVE_ENABLED = process.env.RUN_LIVE_PAID_E2E === "1";
-const LIVE_MAX_DEM = Number(process.env.LIVE_E2E_MAX_DEM ?? "0");
-const LIVE_BUTLER = (process.env.LIVE_BUTLER_ORIGIN ?? "https://butler.agentcommerce.network").replace(/\/$/, "");
-const HARD_MAX_DEM = 5;
-
-function isProcurementPost(request: Request) {
- return request.method() === "POST" && new URL(request.url()).pathname === "/demo/procurement";
-}
-
-test.describe("/try live paid procurement", () => {
- test.describe.configure({ mode: "serial" });
- test.skip(!LIVE_ENABLED, "Set RUN_LIVE_PAID_E2E=1 and LIVE_E2E_MAX_DEM to explicitly authorize one live testnet purchase.");
-
- let context: BrowserContext;
- let page: Page;
- let idempotencyKey = "";
- let jobId = "";
- let submittedInput: Record = {};
- let submittedGoal = "";
- let paymentTx = "";
-
- test.beforeAll(async ({ browser }) => {
- if (!Number.isFinite(LIVE_MAX_DEM) || LIVE_MAX_DEM < 1 || LIVE_MAX_DEM > HARD_MAX_DEM) {
- throw new Error(`LIVE_E2E_MAX_DEM must be between 1 and ${HARD_MAX_DEM}; the suite will not dispatch a paid request otherwise.`);
- }
- context = await browser.newContext();
- page = await context.newPage();
- });
-
- test.afterAll(async () => {
- await context?.close();
- });
-
- test("1. completes one real paid purchase", async ({}, testInfo) => {
- test.setTimeout(13 * 60_000);
- await chooseProcurementExample(page, "pay-dem");
- await page.locator("#proc-budget").fill(String(LIVE_MAX_DEM));
-
- const startResponse = page.waitForResponse((response) => isProcurementPost(response.request()));
- await page.getByRole("button", { name: /Run the full deal/ }).click();
- const response = await startResponse;
- const request = response.request();
- const startBody = await response.json() as { id?: string; error?: unknown };
-
- idempotencyKey = (await request.headerValue("idempotency-key")) ?? "";
- submittedInput = JSON.parse(request.postData() ?? "{}") as Record;
- submittedGoal = String(submittedInput.goal ?? "live E2E procurement");
- jobId = String(startBody.id ?? "");
-
- expect(response.ok(), JSON.stringify(startBody)).toBe(true);
- expect(idempotencyKey).toBeTruthy();
- expect(jobId).toBeTruthy();
- expect(submittedInput.paymentRail).toBe("pay-dem");
- expect(Number(submittedInput.budgetDem)).toBeLessThanOrEqual(LIVE_MAX_DEM);
-
- await expect(page.getByRole("heading", { name: "Security Auditor result" })).toBeVisible({ timeout: 12 * 60_000 });
- paymentTx = (await page.locator(".tx-link code").textContent())?.trim() ?? "";
- expect(paymentTx).toBeTruthy();
-
- await testInfo.attach("live-purchase.json", {
- contentType: "application/json",
- body: Buffer.from(JSON.stringify({ jobId, idempotencyKey, paymentTx, budgetCapDem: LIVE_MAX_DEM }, null, 2)),
- });
- });
-
- test("2. verifies the real post-payment evidence and all five DACS stages", async ({}, testInfo) => {
- await expectAcceptedEvidence(page);
- const response = await context.request.get(`${LIVE_BUTLER}/demo/procurement/${encodeURIComponent(jobId)}`);
- expect(response.ok()).toBe(true);
- const job = await response.json() as Record;
- const result = job.result as Record;
- const settlement = result.settlement as Record;
- const amount = Number(settlement.amountDem ?? settlement.amount);
- const transactions = Array.isArray(result.transactions) ? result.transactions as Array> : [];
-
- expect(amount).toBeGreaterThan(0);
- expect(amount).toBeLessThanOrEqual(LIVE_MAX_DEM);
- expect(String(settlement.txHash)).toBe(paymentTx);
- expect(transactions.some((transaction) => transaction.kind === "payment" && transaction.txRef === paymentTx)).toBe(true);
-
- await testInfo.attach("live-evidence.json", {
- contentType: "application/json",
- body: Buffer.from(JSON.stringify({ jobId, paymentTx, amountDem: amount, events: job.events, result }, null, 2)),
- });
- });
-
- test("3. reload recovery reads the existing job and sends no second POST", async () => {
- const record = { runId: idempotencyKey, jobId, goal: submittedGoal, input: submittedInput, startedAt: new Date().toISOString() };
- await page.evaluate(({ key, value }) => localStorage.setItem(key, JSON.stringify(value)), { key: PROCUREMENT_RUN_KEY, value: record });
- await page.reload();
- await expect(page.locator(".resume-banner")).toContainText(jobId.slice(0, 8));
-
- let posts = 0;
- const countPosts = (request: Request) => { if (isProcurementPost(request)) posts += 1; };
- page.on("request", countPosts);
- const statusResponse = page.waitForResponse((response) =>
- response.request().method() === "GET" && new URL(response.url()).pathname === `/demo/procurement/${jobId}`,
- );
- await page.getByRole("button", { name: /Check & resume/ }).click();
- expect((await statusResponse).ok()).toBe(true);
- await expect(page.getByRole("heading", { name: "Security Auditor result" })).toBeVisible();
- page.off("request", countPosts);
-
- expect(posts).toBe(0);
- await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY)).toBeNull();
- });
-
- test("4. a real second tab is refused before another paid POST", async () => {
- const protectedRecord = {
- runId: idempotencyKey,
- jobId,
- goal: submittedGoal,
- input: submittedInput,
- startedAt: new Date().toISOString(),
- };
- const serialized = JSON.stringify(protectedRecord);
- await page.evaluate(({ key, value }) => localStorage.setItem(key, value), { key: PROCUREMENT_RUN_KEY, value: serialized });
- const secondTab = await context.newPage();
- await secondTab.goto("/try");
- await expect(secondTab.locator(".resume-banner")).toContainText("still on record");
-
- let posts = 0;
- const countPosts = (request: Request) => { if (isProcurementPost(request)) posts += 1; };
- secondTab.on("request", countPosts);
- await secondTab.getByRole("button", { name: /Security Auditor/ }).first().click();
- await secondTab.getByRole("button", { name: "Load example" }).click();
- await secondTab.getByRole("button", { name: /Run the full deal/ }).click();
-
- await expect(secondTab.locator(".bubble.error")).toContainText("earlier procurement run from this browser is still on record");
- expect(posts).toBe(0);
- expect(await secondTab.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY)).toBe(serialized);
- secondTab.off("request", countPosts);
- await secondTab.close();
- await page.evaluate((key) => localStorage.removeItem(key), PROCUREMENT_RUN_KEY);
- });
-
- test("5. cancelling a queued cross-tab lock sends no paid POST", async () => {
- await page.evaluate((key) => localStorage.removeItem(key), PROCUREMENT_RUN_KEY);
- const actor = await context.newPage();
- await page.goto("/try");
- await chooseProcurementExample(actor);
-
- await page.evaluate((lockName) => {
- const scope = window as typeof window & {
- __liveE2eLockHeld?: boolean;
- __liveE2eReleaseLock?: () => void;
- };
- if (!navigator.locks) throw new Error("Web Locks unavailable in the live E2E browser");
- scope.__liveE2eLockHeld = false;
- void navigator.locks.request(lockName, async () => {
- scope.__liveE2eLockHeld = true;
- await new Promise((resolve) => { scope.__liveE2eReleaseLock = resolve; });
- });
- }, PROCUREMENT_LOCK_NAME);
- await expect.poll(() => page.evaluate(() => Boolean((window as typeof window & { __liveE2eLockHeld?: boolean }).__liveE2eLockHeld))).toBe(true);
-
- let posts = 0;
- const countPosts = (request: Request) => { if (isProcurementPost(request)) posts += 1; };
- actor.on("request", countPosts);
- await actor.getByRole("button", { name: /Run the full deal/ }).click();
- await expect(actor.getByText(/FULL DACS FLOW · DEM · Demos/)).toBeVisible();
- await actor.getByRole("button", { name: /Stop watching/ }).click();
- await page.evaluate(() => (window as typeof window & { __liveE2eReleaseLock?: () => void }).__liveE2eReleaseLock?.());
-
- await expect(actor.locator(".bubble.error")).toContainText("Run cancelled in this browser");
- expect(posts).toBe(0);
- expect(await actor.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY)).toBeNull();
- actor.off("request", countPosts);
- await actor.close();
- });
-});
diff --git a/reference-implementations/dacs-directory/e2e/try-dacs.spec.ts b/reference-implementations/dacs-directory/e2e/try-dacs.spec.ts
deleted file mode 100644
index 7deaf6d..0000000
--- a/reference-implementations/dacs-directory/e2e/try-dacs.spec.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-import { expect, test, type Route } from "@playwright/test";
-import {
- PROCUREMENT_LOCK_NAME,
- PROCUREMENT_RUN_KEY,
- chooseProcurementExample,
- completedJob,
- expectAcceptedEvidence,
- failedSecurityPreviewJob,
- installMockGateway,
- securityPreviewJob,
- x402CompletedJob,
-} from "./try-dacs-fixtures.js";
-
-async function fulfillJson(route: Route, body: unknown) {
- await route.fulfill({
- status: 200,
- contentType: "application/json",
- body: JSON.stringify(body),
- headers: { "access-control-allow-origin": "*" },
- });
-}
-
-test.describe("/try procurement browser safety", () => {
- test("1. completes a paid-style procurement and clears its recovery record", async ({ context, page }) => {
- await installMockGateway(context);
- await chooseProcurementExample(page);
-
- await page.getByRole("button", { name: /Run the full deal/ }).click();
-
- await expectAcceptedEvidence(page);
- await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY)).toBeNull();
- });
-
- test("2. reload recovery reuses the original idempotency key", async ({ context, page }) => {
- const keys: string[] = [];
- let posts = 0;
- await installMockGateway(context, {
- onProcurementPost: async (route) => {
- posts += 1;
- keys.push((await route.request().headerValue("idempotency-key")) ?? "");
- if (posts === 1) await route.abort("failed");
- else await fulfillJson(route, completedJob);
- },
- });
- await chooseProcurementExample(page);
-
- await page.getByRole("button", { name: /Run the full deal/ }).click();
- await expect(page.locator(".bubble.error")).toContainText("Retrying reuses the same idempotency key");
- const storedBeforeReload = await page.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY);
- expect(storedBeforeReload).not.toBeNull();
-
- await page.reload();
- await expect(page.locator(".resume-banner")).toContainText("the job id was never received");
- await page.getByRole("button", { name: /Check & resume/ }).click();
-
- await expectAcceptedEvidence(page);
- expect(posts).toBe(2);
- expect(keys[0]).toBeTruthy();
- expect(keys[1]).toBe(keys[0]);
- });
-
- test("3. a second tab cannot overwrite an active procurement record", async ({ context, page }) => {
- let posts = 0;
- let pendingRoute: Route | undefined;
- let releasePost!: (action: "abort") => void;
- const postGate = new Promise<"abort">((resolve) => { releasePost = resolve; });
- let firstPostSeen!: () => void;
- const firstPost = new Promise((resolve) => { firstPostSeen = resolve; });
-
- await installMockGateway(context, {
- onProcurementPost: async (route) => {
- posts += 1;
- pendingRoute = route;
- firstPostSeen();
- await postGate;
- await route.abort("failed");
- },
- });
-
- const secondTab = await context.newPage();
- await secondTab.goto("/try");
- await expect(secondTab.getByRole("button", { name: /Security Auditor/ }).first()).toBeVisible();
- await chooseProcurementExample(page);
- await page.getByRole("button", { name: /Run the full deal/ }).click();
- await firstPost;
-
- await expect(secondTab.locator(".resume-banner")).toContainText("still on record");
- const recordBefore = await secondTab.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY);
- expect(recordBefore).not.toBeNull();
-
- await secondTab.getByRole("button", { name: /Security Auditor/ }).first().click();
- await secondTab.getByRole("button", { name: "Load example" }).click();
- await secondTab.getByRole("button", { name: /Run the full deal/ }).click();
-
- await expect(secondTab.locator(".bubble.error")).toContainText("earlier procurement run from this browser is still on record");
- expect(posts).toBe(1);
- expect(await secondTab.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY)).toBe(recordBefore);
-
- releasePost("abort");
- await expect.poll(() => pendingRoute === undefined || posts === 1).toBeTruthy();
- await secondTab.close();
- });
-
- test("4. cancelling while queued for the Web Lock never dispatches later", async ({ context }) => {
- let posts = 0;
- await installMockGateway(context, {
- onProcurementPost: async (route) => {
- posts += 1;
- await fulfillJson(route, completedJob);
- },
- });
-
- const lockHolder = await context.newPage();
- const actor = await context.newPage();
- await lockHolder.goto("/try");
- await chooseProcurementExample(actor);
-
- await lockHolder.evaluate((lockName) => {
- const scope = window as typeof window & {
- __e2eLockHeld?: boolean;
- __e2eReleaseLock?: () => void;
- };
- if (!navigator.locks) throw new Error("Web Locks unavailable in the E2E browser");
- scope.__e2eLockHeld = false;
- void navigator.locks.request(lockName, async () => {
- scope.__e2eLockHeld = true;
- await new Promise((resolve) => { scope.__e2eReleaseLock = resolve; });
- });
- }, PROCUREMENT_LOCK_NAME);
- await expect.poll(() => lockHolder.evaluate(() => Boolean((window as typeof window & { __e2eLockHeld?: boolean }).__e2eLockHeld))).toBe(true);
-
- await actor.getByRole("button", { name: /Run the full deal/ }).click();
- await expect(actor.getByText(/FULL DACS FLOW · DEM · Demos/)).toBeVisible();
- await actor.getByRole("button", { name: /Stop watching/ }).click();
- await lockHolder.evaluate(() => (window as typeof window & { __e2eReleaseLock?: () => void }).__e2eReleaseLock?.());
-
- await expect(actor.locator(".bubble.error")).toContainText("Run cancelled in this browser");
- expect(posts).toBe(0);
- expect(await actor.evaluate((key) => localStorage.getItem(key), PROCUREMENT_RUN_KEY)).toBeNull();
- });
-
- test("5. renders all five stages and the post-payment evidence", async ({ context, page }) => {
- const runningJob = {
- id: completedJob.id,
- status: "running",
- phase: "discovering",
- events: completedJob.events.slice(0, 1),
- };
- await installMockGateway(context, {
- onProcurementPost: (route) => fulfillJson(route, runningJob),
- onProcurementGet: (route) => fulfillJson(route, completedJob),
- });
- await chooseProcurementExample(page);
-
- await page.getByRole("button", { name: /Run the full deal/ }).click();
-
- await expectAcceptedEvidence(page);
- await expect(page.locator(".tx-link code")).toHaveText("mock-payment-transaction");
- await expect(page.locator(".chain-activity .chain-row")).toHaveCount(completedJob.events.length);
- await expect(page.getByText("Full evidence bundle accepted & reconciled", { exact: true })).toBeVisible();
- });
-
- test("6. switches to the x402 schema and submits the USDC rail explicitly", async ({ context, page }) => {
- let submitted: Record | undefined;
- await installMockGateway(context, {
- onProcurementPost: async (route) => {
- submitted = JSON.parse(route.request().postData() ?? "{}") as Record;
- await fulfillJson(route, x402CompletedJob);
- },
- });
- await chooseProcurementExample(page, "pay-x402");
-
- await expect(page.getByText("Operator-provisional rail authority", { exact: true })).toBeVisible();
- await expect(page.getByLabel("USDC budget")).toHaveValue("0.1");
- await page.getByRole("button", { name: /Run the full deal/ }).click();
-
- await expect(page.getByRole("heading", { name: "Security Auditor result" })).toBeVisible();
- await expect(page.getByRole("heading", { name: "Base Sepolia USDC settlement" })).toBeVisible();
- await expect(page.getByText("settled & seller-verified", { exact: true })).toBeVisible();
- await expect(page.getByText("Settled & accepted", { exact: true })).toBeVisible();
- expect(submitted?.profileId).toBe("security-audit-rfq");
- expect(submitted?.paymentRail).toBe("pay-x402");
- expect(submitted?.budgetUsdc).toBe(0.1);
- expect(submitted?.budgetDem).toBeUndefined();
- });
-
- test("7. required Oracle parameters block dispatch and identify the invalid field", async ({ context, page }) => {
- await installMockGateway(context);
- await page.goto("/try");
- await page.getByRole("button", { name: /Oracle Desk/ }).first().click();
-
- const run = page.getByRole("button", { name: /Run the full deal/ });
- const coin = page.getByLabel("Coin");
- await expect(run).toBeEnabled();
- await coin.fill("");
- await expect(run).toBeDisabled();
- await expect(coin).toHaveAttribute("aria-invalid", "true");
- await expect(page.getByText("Required — enter a CoinGecko coin id.")).toBeVisible();
-
- await page.getByLabel("Data product").selectOption("fx-rate");
- const base = page.getByLabel("From currency");
- const quote = page.getByLabel("To currency");
- await expect(run).toBeEnabled();
-
- await base.fill("");
- await expect(run).toBeDisabled();
- await expect(base).toHaveAttribute("aria-invalid", "true");
- await expect(page.getByText("Required — enter the currency to convert from.")).toBeVisible();
-
- await base.fill("USD");
- await quote.fill("");
- await expect(run).toBeDisabled();
- await expect(quote).toHaveAttribute("aria-invalid", "true");
- await expect(page.getByText("Required — enter the currency to convert to.")).toBeVisible();
- });
-
- test("8. shows verified delivery while DACS-5 remains nonterminal, then accepts only the completed job", async ({ context, page }) => {
- let releaseFinalPoll!: () => void;
- const finalPollGate = new Promise((resolve) => { releaseFinalPoll = resolve; });
- let previewReturned!: () => void;
- const previewSeen = new Promise((resolve) => { previewReturned = resolve; });
-
- await installMockGateway(context, {
- onProcurementPost: async (route) => {
- await fulfillJson(route, securityPreviewJob);
- previewReturned();
- },
- onProcurementGet: async (route) => {
- await finalPollGate;
- await fulfillJson(route, completedJob);
- },
- });
- await chooseProcurementExample(page);
-
- await page.getByRole("button", { name: /Run the full deal/ }).click();
- await previewSeen;
-
- await expect(page.getByRole("heading", { name: "Verified security report is ready" })).toBeVisible();
- await expect(page.getByText("DACS-5 finalising", { exact: true })).toBeVisible();
- await expect(page.getByText("Not settled-and-accepted yet", { exact: true })).toBeVisible();
- await expect(page.getByText("Unsafe dynamic execution", { exact: true })).toBeVisible();
- await expect(page.getByText(/DELIVERY VERIFIED · DACS-5 FINALISING/)).toBeVisible();
- await expect(page.locator(".journey-step.active").filter({ hasText: "Verify" })).toContainText("two DACS-5 copies are finalising");
- await expect(page.locator(".journey-step.active")).toHaveCount(1);
- await expect(page.getByRole("heading", { name: "Security Auditor result" })).toHaveCount(0);
- await expect(page.getByText("Settled & accepted", { exact: true })).toHaveCount(0);
-
- releaseFinalPoll();
- await expectAcceptedEvidence(page);
- await expect(page.getByRole("heading", { name: "Verified security report is ready" })).toHaveCount(0);
- });
-
- test("9. preserves a verified delivery when DACS-5 finalisation fails without claiming acceptance", async ({ context, page }) => {
- await installMockGateway(context, {
- onProcurementPost: (route) => fulfillJson(route, securityPreviewJob),
- onProcurementGet: (route) => fulfillJson(route, failedSecurityPreviewJob),
- });
- await chooseProcurementExample(page);
-
- await page.getByRole("button", { name: /Run the full deal/ }).click();
-
- await expect(page.getByRole("heading", { name: "Verified security report is ready" })).toBeVisible();
- await expect(page.getByText("DACS-5 needs recovery", { exact: true })).toBeVisible();
- await expect(page.getByText("Finalisation stopped safely", { exact: true })).toBeVisible();
- await expect(page.getByText(failedSecurityPreviewJob.error, { exact: true }).first()).toBeVisible();
- await expect(page.getByText("Unsafe dynamic execution", { exact: true })).toBeVisible();
- await expect(page.getByText("Not settled-and-accepted yet", { exact: true })).toBeVisible();
- await expect(page.getByText("Settled & accepted", { exact: true })).toHaveCount(0);
- await expect(page.getByRole("heading", { name: "Security Auditor result" })).toHaveCount(0);
- await expect(page.getByText(/Retrying reuses the same idempotency key/)).toHaveCount(0);
- });
-});
diff --git a/reference-implementations/dacs-directory/package.json b/reference-implementations/dacs-directory/package.json
index f3937b4..4b174b1 100644
--- a/reference-implementations/dacs-directory/package.json
+++ b/reference-implementations/dacs-directory/package.json
@@ -10,11 +10,9 @@
"build": "next build",
"start": "next start -p ${PORT:-3400}",
"start:railway": "bash scripts/start-railway.sh",
- "check:deploy-config": "node scripts/check-butler-origin.mjs",
- "check:butler": "node scripts/check-butler-origin.mjs --probe",
+ "check:deploy-config": "node scripts/check-directory-origin.mjs",
"test": "tsx --test test/*.test.ts test/*.test.mjs",
- "test:e2e": "playwright test e2e/home.spec.ts e2e/register.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": "playwright test e2e/home.spec.ts e2e/register.spec.ts",
"test:e2e:ui": "playwright test --ui",
"test:seed": "tsx --test test/seed-smoke.test.ts",
"index": "tsx src/catalog/reindex.ts",
diff --git a/reference-implementations/dacs-directory/playwright.config.ts b/reference-implementations/dacs-directory/playwright.config.ts
index e25f23e..cd31cb0 100644
--- a/reference-implementations/dacs-directory/playwright.config.ts
+++ b/reference-implementations/dacs-directory/playwright.config.ts
@@ -1,16 +1,12 @@
import { defineConfig, devices } from "@playwright/test";
-const butlerOrigin = process.env.LIVE_BUTLER_ORIGIN ?? "https://butler.agentcommerce.network";
-const livePaidRun = process.env.RUN_LIVE_PAID_E2E === "1";
-
export default defineConfig({
testDir: "./e2e",
- outputDir: livePaidRun ? "test-results/playwright-live" : "test-results/playwright",
+ outputDir: "test-results/playwright",
fullyParallel: false,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
- workers: livePaidRun ? 1 : undefined,
- reporter: [["list"], ["html", { open: "never", outputFolder: livePaidRun ? "playwright-report-live" : "playwright-report" }]],
+ reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report" }]],
use: {
baseURL: "http://localhost:3400",
trace: "retain-on-failure",
@@ -25,12 +21,11 @@ export default defineConfig({
],
webServer: {
command: "npm run dev",
- url: "http://localhost:3400/try",
+ url: "http://localhost:3400/",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
env: {
NEXT_PUBLIC_DIRECTORY_URL: "http://localhost:3400",
- NEXT_PUBLIC_BUTLER_ORIGIN: butlerOrigin,
},
},
});
diff --git a/reference-implementations/dacs-directory/scripts/check-butler-origin.mjs b/reference-implementations/dacs-directory/scripts/check-butler-origin.mjs
deleted file mode 100644
index b2223c2..0000000
--- a/reference-implementations/dacs-directory/scripts/check-butler-origin.mjs
+++ /dev/null
@@ -1,80 +0,0 @@
-import { resolve } from "node:path";
-import { pathToFileURL } from "node:url";
-
-export function httpsOrigin(env, name) {
- const raw = env[name]?.trim();
- if (!raw) throw new Error(`${name} is required for a production deployment`);
- let url;
- try { url = new URL(raw); }
- catch { throw new Error(`${name} must be an absolute URL`); }
- if (url.protocol !== "https:") throw new Error(`${name} must use HTTPS`);
- if (url.username || url.password || url.search || url.hash || (url.pathname !== "/" && url.pathname !== "")) {
- throw new Error(`${name} must be an origin without credentials, path, query, or fragment`);
- }
- return url.origin;
-}
-
-function headerValues(response, name) {
- return (response.headers.get(name) ?? "").toLowerCase().split(",").map((value) => value.trim()).filter(Boolean);
-}
-
-export function assertCorsOrigin(response, directoryOrigin, requestLabel) {
- if (response.headers.get("access-control-allow-origin") !== directoryOrigin) {
- throw new Error(`Butler gateway does not CORS-allow ${directoryOrigin} for ${requestLabel}`);
- }
-}
-
-export function assertJsonPostPreflight(response, directoryOrigin) {
- if (!response.ok) throw new Error(`Butler JSON POST preflight returned HTTP ${response.status}`);
- assertCorsOrigin(response, directoryOrigin, "JSON POST preflight");
- if (!headerValues(response, "access-control-allow-methods").includes("post")) {
- throw new Error("Butler JSON POST preflight does not allow POST");
- }
- if (!headerValues(response, "access-control-allow-headers").includes("content-type")) {
- throw new Error("Butler JSON POST preflight does not allow content-type");
- }
-}
-
-export async function checkButlerOrigin({ env = process.env, fetcher = fetch, probe = false } = {}) {
- const directoryOrigin = httpsOrigin(env, "NEXT_PUBLIC_DIRECTORY_URL");
- const butlerOrigin = httpsOrigin(env, "NEXT_PUBLIC_BUTLER_ORIGIN");
-
- if (!probe) return `Production origins valid: directory=${directoryOrigin} butler=${butlerOrigin}`;
-
- const response = await fetcher(`${butlerOrigin}/demo/butler/agents`, {
- headers: { origin: directoryOrigin },
- signal: AbortSignal.timeout(10_000),
- });
- if (!response.ok) throw new Error(`Butler catalog probe returned HTTP ${response.status}`);
- assertCorsOrigin(response, directoryOrigin, "catalog GET");
- const body = await response.json();
- if (!Array.isArray(body?.agents) || body.agents.length === 0) {
- throw new Error("Butler catalog probe returned no agents");
- }
-
- for (const path of ["/demo/procurement", "/demo/butler"]) {
- const preflight = await fetcher(`${butlerOrigin}${path}`, {
- method: "OPTIONS",
- headers: {
- origin: directoryOrigin,
- "access-control-request-method": "POST",
- "access-control-request-headers": "content-type",
- },
- signal: AbortSignal.timeout(10_000),
- });
- try { assertJsonPostPreflight(preflight, directoryOrigin); }
- catch (cause) { throw new Error(`${path}: ${cause instanceof Error ? cause.message : String(cause)}`); }
- }
- return `Butler gateway ready: ${body.agents.length} agents and JSON POST CORS available to ${directoryOrigin}`;
-}
-
-async function main() {
- try {
- console.log(await checkButlerOrigin({ probe: process.argv.includes("--probe") }));
- } catch (error) {
- console.error(`[deployment config] ${error.message}`);
- process.exitCode = 1;
- }
-}
-
-if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) await main();
diff --git a/reference-implementations/dacs-directory/scripts/check-directory-origin.mjs b/reference-implementations/dacs-directory/scripts/check-directory-origin.mjs
new file mode 100644
index 0000000..346ceb7
--- /dev/null
+++ b/reference-implementations/dacs-directory/scripts/check-directory-origin.mjs
@@ -0,0 +1,42 @@
+import { resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+
+export function httpsOrigin(env, name) {
+ const raw = env[name]?.trim();
+ if (!raw) throw new Error(`${name} is required for a production deployment`);
+
+ let url;
+ try {
+ url = new URL(raw);
+ } catch {
+ throw new Error(`${name} must be an absolute URL`);
+ }
+
+ if (url.protocol !== "https:") throw new Error(`${name} must use HTTPS`);
+ if (
+ url.username
+ || url.password
+ || url.search
+ || url.hash
+ || (url.pathname !== "/" && url.pathname !== "")
+ ) {
+ throw new Error(`${name} must be an origin without credentials, path, query, or fragment`);
+ }
+ return url.origin;
+}
+
+export function checkDirectoryOrigin({ env = process.env } = {}) {
+ const directoryOrigin = httpsOrigin(env, "NEXT_PUBLIC_DIRECTORY_URL");
+ return `Production directory origin valid: ${directoryOrigin}`;
+}
+
+async function main() {
+ try {
+ console.log(checkDirectoryOrigin());
+ } catch (error) {
+ console.error(`[deployment config] ${error instanceof Error ? error.message : String(error)}`);
+ process.exitCode = 1;
+ }
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) await main();
diff --git a/reference-implementations/dacs-directory/src/components/HomeDealDemo.tsx b/reference-implementations/dacs-directory/src/components/HomeDealDemo.tsx
deleted file mode 100644
index 6381e99..0000000
--- a/reference-implementations/dacs-directory/src/components/HomeDealDemo.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-"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}
-
- ))}
-
- 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 === "replay" && }
-
- Run a live deal choose agent · DEM or x402
-
-
-
-
-
-
- Recorded RFQ replay · job d27cd332 · 20 July 2026
- Replays one completed purchase — never starts a job or spends funds. Run one live at Try DACS.
-
-
-
- {STAGES.map((stage, index) => {
- const state = complete || currentStage > index ? "done" : (shown.length && currentStage === index ? "active" : "todo");
- return (
-
The Butler got its audit, the Auditor got paid, and the entire deal is now a chain of signed receipts anyone can re-check — the listing, the identity vet, the signed terms, the payment, and the delivery. That is DACS.
{evidence.deliveryVerified ? String(record(audit.summary).text ?? "The delivered report was verified and contains no findings.") : "No verified report was returned."}
{String(summary.text ?? "The signed deliverable is verified and available in the raw preview below.")}
- )}
-
Not settled-and-accepted yetDACS-5 is still mandatory. The Directory will only show final acceptance after the buyer and seller bundles verify and reconcile.
- Verified preview anchors
{anchorEntries.map(([name, anchor]) =>
{name}{anchor}
)}
- Raw verified delivery preview
{JSON.stringify(preview, null, 2)}
-
- );
-}
-
-/**
- * The five DACS stages, in the standard's own words (see /how-it-works:
- * Identify → Vet → Negotiate → Settle → Verify, "one deal · five receipts").
- * `plain` is the novice explanation shown behind "What's happening here?".
- */
-const DACS_STAGES = [
- {
- key: "identify", name: "Identify", primitive: "DACS-1",
- tagline: "Who offers the service? Signed listings only.",
- receipt: "a signed on-chain listing",
- plain: "Agents advertise what they offer. In full DACS that's a signed listing anchored on the Demos chain — like a business card that can't be forged. This demo's picker shows the live gateway's agent roster; the Procurement Butler run then resolves and verifies a real signed DACS-1 listing on-chain before dealing.",
- },
- {
- key: "vet", name: "Vet", primitive: "DACS-2",
- tagline: "Check the counterparty before committing.",
- receipt: "an anchored verification record",
- plain: "Before dealing with a stranger you check who they are. Here the buyer verifies the seller's identity and claims, and writes that check to the chain — so later everyone can see the vet actually happened.",
- },
- {
- key: "negotiate", name: "Negotiate", primitive: "DACS-3",
- tagline: "Fix price, scope and timing — both agents sign.",
- receipt: "a dual-signed agreement",
- plain: "The two agents agree the job: what will be done, by when, for how much. Both sign the same agreement, so neither can later claim different terms. That signed agreement is anchored before any money moves.",
- },
- {
- key: "settle", name: "Settle & deliver", primitive: "DACS-4",
- tagline: "Pay on the agreed rail; the work arrives with evidence.",
- receipt: "a payment transaction + delivery evidence",
- plain: "The buyer pays on the agreed payment rail — native DEM on Demos or USDC through x402 on Base Sepolia — and the seller delivers the work. The payment hash and the delivered report are both captured as evidence.",
- },
- {
- key: "verify", name: "Verify", primitive: "DACS-5",
- tagline: "Bind every receipt into one bundle both sides anchor.",
- receipt: "a reconciled attestation bundle",
- plain: "Finally everything — listing, vet, agreement, payment and delivery — is tied into one signed bundle that both sides anchor. Anyone can re-run the checks later, and honest deals build the seller's public reputation.",
- },
-] as const;
-
-type StageOutput = {
- state: "pending" | "active" | "complete" | "warning" | "skipped";
- summary: string;
- detail?: string;
- /** Chain records produced by this stage, rendered as explorer links. */
- chain?: ProcurementEvent[];
-};
-
-function message(value: unknown): string {
- if (value && typeof value === "object" && "error" in value) {
- const error = (value as { error?: string | { message?: string } }).error;
- if (typeof error === "string" && error.trim()) return error;
- if (error && typeof error === "object" && error.message) return error.message;
- }
- return "The Butler could not complete that step.";
-}
-
-export default function TryDacs() {
- const [agents, setAgents] = useState([]);
- const [profiles, setProfiles] = useState([]);
- const [selectedProfileId, setSelectedProfileId] = useState(null);
- const [selectedPaymentRail, setSelectedPaymentRail] = useState("pay-dem");
- const [goal, setGoal] = useState("");
- const [plan, setPlan] = useState(null);
- const [inputValue, setInputValue] = useState>({});
- const [gatewayFieldErrors, setGatewayFieldErrors] = useState({});
- const [submittedSummary, setSubmittedSummary] = useState([]);
- const [phase, setPhase] = useState<"idle" | "planning" | "ready" | "running" | "done" | "error">("idle");
- const [result, setResult] = useState();
- const [procurementJob, setProcurementJob] = useState(null);
- const [error, setError] = useState("");
- const [runStartedAt, setRunStartedAt] = useState(null);
- const [elapsedMs, setElapsedMs] = useState(0);
- const [receipt, setReceipt] = useState(null);
- const [receiptPolling, setReceiptPolling] = useState(false);
- const [receiptMessage, setReceiptMessage] = useState("");
- const runAbort = useRef(null);
- const receiptAbort = useRef(null);
- const procurementJobRef = useRef(null);
- // One idempotency key per intentional procurement run, reused across retries
- // of that run so the gateway returns the existing job instead of creating a
- // second paid one. Cleared when a new intent begins (new agent, edited input,
- // Load example) or on success.
- const procurementRunId = useRef(null);
-
- useEffect(() => {
- fetch(`${BUTLER}/demo/procurement/options`)
- .then((res) => res.ok ? res.json() : Promise.reject(new Error("procurement options unavailable")))
- .then((body: unknown) => {
- const live = parseProcurementProfiles(body)
- .filter((profile) => profile.executable && LIVE_PROCUREMENT_PROFILE_IDS.includes(profile.id));
- if (live.length !== 3 || new Set(live.map((profile) => profile.id)).size !== 3) {
- throw new Error("the three production procurement profiles are not all executable");
- }
- setProfiles(live);
- setAgents(live.map((profile) => procurementProfileCard(profile, defaultPaymentRail(profile))));
- })
- .catch((cause: unknown) => setError(cause instanceof ButlerContractError
- ? cause.message
- : "The live procurement demos are temporarily unavailable."));
- }, []);
-
- useEffect(() => {
- if (!runStartedAt || (phase !== "running" && !receiptPolling)) return;
- const update = () => setElapsedMs(Date.now() - runStartedAt);
- update();
- const timer = setInterval(update, 100);
- return () => clearInterval(timer);
- }, [phase, receiptPolling, runStartedAt]);
-
- useEffect(() => { procurementJobRef.current = procurementJob; }, [procurementJob]);
-
- // The persisted procurement run (idempotency key + input + job id). Written
- // BEFORE the POST is dispatched so a reload/crash can never lose the key of
- // a purchase the gateway may have started; kept until the job reaches a
- // browser-verified safe terminal state.
- const [storedRun, setStoredRun] = useState(null);
- const updateStoredRun = useCallback((run: StoredProcurementRun | null): boolean => {
- const ok = writeStoredRun(run);
- // Reflect what storage actually holds, not what we hoped to write.
- setStoredRun(ok ? run : readStoredRun());
- return ok;
- }, []);
- /** Ownership-conditional delete: never removes another tab's record. */
- const releaseStoredRun = useCallback((runId: string | null) => {
- releaseStoredRunIfOwned(runId);
- setStoredRun(readStoredRun());
- }, []);
- useEffect(() => { setStoredRun(readStoredRun()); }, []);
- // Keep this tab's view of the record in sync when ANOTHER tab writes or
- // clears it (storage events fire only in non-originating tabs).
- useEffect(() => {
- const onStorage = (event: StorageEvent) => {
- if (event.key === PROCUREMENT_RUN_KEY || event.key === null) setStoredRun(readStoredRun());
- };
- window.addEventListener("storage", onStorage);
- return () => window.removeEventListener("storage", onStorage);
- }, []);
-
- useEffect(() => () => {
- runAbort.current?.abort();
- receiptAbort.current?.abort();
- }, []);
-
- const selectedProfile = useMemo(() => profiles.find((profile) => profile.id === selectedProfileId), [profiles, selectedProfileId]);
- const selected = useMemo(() => selectedProfile
- ? procurementProfileCard(selectedProfile, selectedPaymentRail)
- : agents.find((agent) => agent.name === plan?.butler.selectedAgent),
- [agents, plan, selectedPaymentRail, selectedProfile]);
- const selectedRailReadiness = selectedProfile?.railReadiness[selectedPaymentRail];
- const execution = record(record(result).execution);
- const specialistDurationMs = typeof execution.durationMs === "number" ? execution.durationMs : undefined;
- const receiptElapsedMs = receipt?.createdAt
- ? Math.max(0, (receipt.status === "confirmed" || receipt.status === "failed" ? Date.parse(receipt.updatedAt ?? receipt.createdAt) : Date.now()) - Date.parse(receipt.createdAt))
- : 0;
- const procurementAccepted = selectedProfile !== undefined && result !== undefined
- ? procurementEvidence(result, selectedProfile.mode).overallAccepted
- : false;
- const validateInput = useCallback((agentName: string, value: Record): FieldErrors => {
- if (hasBuiltinForm(agentName)) return validateAgentInput(agentName, value);
- const schema = selected ? parseAgentFieldSchema(selected) : null;
- return schema ? validateSchemaInput(schema, value) : {};
- }, [selected]);
- const localErrors = useMemo(
- () => plan ? validateInput(plan.butler.selectedAgent, inputValue) : {},
- [plan, inputValue, validateInput],
- );
- const inputIsValid = Object.keys(localErrors).length === 0 && (selectedRailReadiness?.executable ?? true);
- // Verification completes only on evidence: a confirmed receipt (or no
- // receipt advertised at all). A synchronous broadcast-only attestation has
- // no status URL to poll, so its confirmation is UNKNOWN in this browser —
- // the Verify phase stays visibly pending rather than claiming completion.
- const verificationComplete = phase === "done" && procurementAccepted;
- const resultPayload = result;
- const resultFields = Object.keys(record(resultPayload));
- const isProcurementSel = selectedProfileId !== null;
- const procEvents = procurementJob?.events ?? [];
- const procurementPreview = procurementJob?.preview;
- const procurementWaiting = phase === "running" && procurementJob?.queue?.status === "waiting";
- // Fail-closed staging: a terminal "failed" (or unknown-phase) event attaches
- // to the stage the run had actually reached and never advances progress.
- const { byStage: eventsByStage, progress: procStage } = stageEvents(procEvents);
-
- // Live procurement run: each DACS stage reports the gateway's own events
- // (and their chain records) as they arrive.
- function procurementStageOutput(stageIdx: number): StageOutput {
- const events = eventsByStage[stageIdx]!;
- const latest = events[events.length - 1];
- const chain = events.filter((event) => event.txRef || event.anchorRef);
- const jobComplete = procurementJob?.status === "complete";
- const jobFailed = procurementJob?.status === "failed" || phase === "error";
- if (stageIdx === 3 && procurementPreview) {
- return {
- state: "complete",
- summary: "Payment evidence and seller delivery verified",
- detail: "The verified result is available; two-sided DACS-5 reconciliation continues separately",
- chain,
- };
- }
- if (stageIdx === 4 && procurementPreview && procurementJob?.status === "running") {
- return {
- state: "active",
- summary: "Verified delivery ready; two DACS-5 copies are finalising",
- detail: "Completion remains pending until both bundles reconcile",
- chain,
- };
- }
- if (stageIdx === 4 && jobComplete) {
- return {
- state: procurementAccepted ? "complete" : "warning",
- summary: procurementAccepted ? "Full evidence bundle accepted & reconciled" : "Verification incomplete",
- detail: latest?.label,
- chain,
- };
- }
- if (procStage > stageIdx || jobComplete) {
- return { state: "complete", summary: latest?.label ?? "Completed", detail: events.length > 1 ? `${events.length} steps recorded` : undefined, chain };
- }
- if (procStage === stageIdx) {
- return jobFailed
- ? { state: "warning", summary: latest?.label ?? "Stopped safely", detail: error || undefined, chain }
- : { state: "active", summary: latest?.label ?? "In progress", detail: `${elapsedLabel(elapsedMs)} elapsed`, chain };
- }
- return { state: "pending", summary: "Waiting for the earlier stages" };
- }
-
- const identifyOut: StageOutput = !agents.length
- ? { state: "active", summary: "Reading the live procurement profiles…" }
- : !plan
- ? { state: "active", summary: `${agents.length} live procurement routes — pick one`, detail: agents.map((agent) => agent.label).join(", ") }
- : isProcurementSel
- ? { state: "pending", summary: `${selected?.label ?? "The seller"}'s signed DACS-1 listing is resolved and verified when the run starts` }
- : { state: "skipped", summary: `${plan.butler.label} picked from the gateway roster — no DACS-1 listing involved`, detail: "This picker is the gateway's published roster, not a signed listing. A real DACS-1 listing is only resolved and verified during the full Procurement run." };
- const vetOut: StageOutput = !plan
- ? { state: "pending", summary: "Waiting for an agent choice" }
- : isProcurementSel
- ? { state: "pending", summary: "Runs live during the purchase — the vet record is anchored on-chain" }
- : { state: "skipped", summary: "Skipped in this demo", detail: "The Butler already trusts this in-network specialist. Run the Procurement Butler to watch a real counterparty vet get anchored as a DACS-2 record." };
- const negotiateOut: StageOutput = !plan
- ? { state: "pending", summary: "Waiting for an agent choice" }
- : isProcurementSel
- ? { state: "pending", summary: "Runs live during the purchase — both agents sign the agreement" }
- : { state: "skipped", summary: "Skipped — this demo is free", detail: "There is nothing to price here. The Procurement Butler negotiates a real RFQ and both agents sign the DACS-3 agreement before payment." };
- const settleOut: StageOutput = phase === "running"
- ? { state: "active", summary: `Live procurement running · ${elapsedLabel(elapsedMs)}`, detail: `The gateway confirms the signed agreement before broadcasting the real ${paymentRailLabel(selectedPaymentRail)} payment.` }
- : phase === "done"
- ? { state: "complete", summary: specialistDurationMs === undefined ? "Result delivered" : `Result delivered in ${elapsedLabel(specialistDurationMs)}`, detail: resultFields.length ? `Result fields: ${resultFields.join(", ")}` : "The complete result is shown below." }
- : phase === "error"
- ? { state: "warning", summary: "Execution stopped safely", detail: error }
- : plan && inputIsValid
- ? { state: "pending", summary: `A real ${paymentRailLabel(selectedPaymentRail)} payment happens here when you press Run` }
- : plan
- ? { state: "active", summary: "Waiting for required fields", detail: `${Object.keys(localErrors).length} field${Object.keys(localErrors).length === 1 ? " needs" : "s need"} attention in the form` }
- : { state: "pending", summary: "Waiting for job details" };
- // A specialist's output attestation is real evidence, but it is a
- // single-sided anchor — NOT the two-party DACS-5 bundle this stage names —
- // so the stage never earns a completion tick outside the full purchase.
- const verifyOut: StageOutput = receipt
- ? {
- state: "skipped",
- summary: `Single-sided output anchor ${receipt.status} — not a DACS-5 bundle${!receipt.statusUrl && receipt.status !== "confirmed" && receipt.status !== "failed" ? " · confirmation unknown (anchoring continues on the gateway)" : ""}`,
- detail: `${receipt.txRef ? `Transaction ${compact(receipt.txRef, 14, 7)} — inspect it in Chain activity below.` : `Anchor ${compact(receipt.anchorAddress, 18, 8)}.`}${receipt.status === "failed" ? " Anchoring failed safely — use the receipt panel below to retry." : ""} The reconciled two-party DACS-5 bundle is only produced by the full Procurement run.`,
- }
- : phase === "done"
- ? { state: "skipped", summary: "No DACS-5 bundle — this demo returns the result directly", detail: "This gateway did not advertise a separate live receipt for this agent. The reconciled DACS-5 bundle is only produced by the full Procurement run." }
- : { state: "pending", summary: "Waiting for delivery" };
-
- // A live/finished procurement job reports the real gateway events per stage;
- // everything else (specialists, and procurement before the run) uses the
- // honest static mapping — including visibly-skipped stages.
- const stageOutputs: StageOutput[] = isProcurementSel && procEvents.length
- ? DACS_STAGES.map((_, stageIdx) => procurementStageOutput(stageIdx))
- : [identifyOut, vetOut, negotiateOut, settleOut, verifyOut];
-
- // Every chain record seen this run (procurement events + the specialist
- // receipt), so all transactions stay visible in one place with explorer links.
- const chainActivity: Array<{ label: string; txRef?: string; anchorRef?: string }> = [
- ...procEvents.filter((event) => event.txRef || event.anchorRef).map((event) => ({ label: event.label, txRef: event.txRef, anchorRef: event.anchorRef })),
- ...(receipt ? [{ label: "Output attestation anchored", txRef: receipt.txRef, anchorRef: receipt.anchorAddress }] : []),
- ];
- const chainTxCount = chainActivity.filter((row) => row.txRef).length;
-
- async function watchReceipt(initial: OutputReceipt) {
- // The synchronous LIVE-ANCHOR attestation has no status URL: there is
- // nothing to poll — the anchor status shown is already the final report
- // from the run response.
- const statusUrl = initial.statusUrl;
- if (!statusUrl) return;
- receiptAbort.current?.abort();
- const controller = new AbortController();
- receiptAbort.current = controller;
- setReceiptPolling(true); setReceiptMessage("");
- let current = initial;
- const deadline = Date.now() + RECEIPT_WATCH_TIMEOUT_MS;
- try {
- while (current.status !== "confirmed" && current.status !== "failed") {
- if (Date.now() >= deadline) {
- setReceiptMessage("Receipt confirmation is taking longer than two minutes. The result is safe; you can check again without rerunning the agent.");
- return;
- }
- await waitWithSignal(Math.min(1_500, deadline - Date.now()), controller.signal);
- const { response, body } = await fetchJsonBeforeDeadline(
- new URL(current.statusUrl ?? statusUrl, `${BUTLER}/`),
- { signal: controller.signal },
- deadline,
- fetch,
- "Receipt status checking exceeded two minutes.",
- );
- if (!response.ok) throw new Error(message(body));
- current = parseReceiptEnvelope(body);
- setReceipt(current);
- }
- if (current.status === "failed") setReceiptMessage(current.error ?? "Receipt anchoring failed safely. Retry will reuse the same receipt and wallet queue.");
- } catch (cause) {
- if ((cause as Error).name !== "AbortError") setReceiptMessage((cause as Error).message);
- } finally {
- if (receiptAbort.current === controller) {
- receiptAbort.current = null;
- setReceiptPolling(false);
- }
- }
- }
-
- async function retryReceipt() {
- if (!receipt?.statusUrl) return;
- try {
- let next = receipt;
- if (receipt.status === "failed") {
- const { response, body } = await fetchJsonWithTimeout(
- new URL(`${receipt.statusUrl}/retry`, `${BUTLER}/`),
- { method: "POST", headers: { "content-type": "application/json" }, body: "{}" },
- 15_000,
- "The receipt retry request timed out.",
- );
- if (!response.ok) throw new Error(message(body));
- next = parseReceiptEnvelope(body);
- setReceipt(next);
- }
- void watchReceipt(next);
- } catch (cause) {
- setReceiptMessage((cause as Error).message);
- }
- }
-
- function cancelRun() {
- runAbort.current?.abort();
- }
-
- function cancelReceiptWatch() {
- receiptAbort.current?.abort();
- setReceiptPolling(false);
- setReceiptMessage("Stopped checking in this browser. The gateway will continue nonce-safe anchoring in the background.");
- }
-
- async function runAgent() {
- if (!plan) return;
- let parsed: Record;
- try { parsed = parseAgentInput(inputValue); }
- catch (cause) {
- setError((cause as Error).message);
- return;
- }
- // Local validation is advisory feedback; a hard local failure blocks the
- // obviously-broken submissions, the gateway remains authoritative.
- const advisory = validateInput(plan.butler.selectedAgent, parsed);
- if (Object.keys(advisory).length > 0) {
- setError("Some fields need attention before this can run — see the highlighted inputs.");
- return;
- }
- // A new procurement run must not overwrite the persisted record of an
- // earlier, unreconciled one — that record may be the only handle on a
- // paid job. Retrying the SAME run (matching key) passes through.
- if (isProcurementSel && storedRun && storedRun.runId !== procurementRunId.current) {
- setError("An earlier procurement run from this browser is still on record. Use “Check & resume” in the banner above (or dismiss it once reconciled) before starting a new purchase.");
- return;
- }
- runAbort.current?.abort(); receiptAbort.current?.abort();
- const controller = new AbortController();
- runAbort.current = controller;
- setPhase("running"); setError(""); setResult(undefined); setReceipt(null); setReceiptMessage("");
- // Clear any prior job before a fresh start so a failed start can never
- // offer resuming a stale job (ref set synchronously: the catch below may
- // run before the state-sync effect).
- setProcurementJob(null); procurementJobRef.current = null;
- setGatewayFieldErrors({});
- setSubmittedSummary(summarizeAgentInput(plan.butler.selectedAgent, parsed));
- setRunStartedAt(Date.now()); setElapsedMs(0);
- // A 4xx whose details name specific fields returns the user to the form
- // with the gateway's own words beside the inputs (authoritative).
- const fieldMappedRejection = (body: unknown): boolean => {
- const envelope = record(record(body).error);
- const gatewayMessage = typeof envelope.message === "string" ? envelope.message : "";
- const details = Array.isArray(envelope.details) ? envelope.details.filter((line): line is string => typeof line === "string") : undefined;
- if (!gatewayMessage) return false;
- const mapped = mapGatewayErrors(gatewayMessage, details, flattenInputKeys(parsed));
- if (Object.keys(mapped.byField).length === 0) return false;
- setGatewayFieldErrors(mapped.byField);
- setError([gatewayMessage, ...mapped.global].filter(Boolean).join(" — "));
- setPhase("ready");
- return true;
- };
- try {
- if (isProcurementSel && selectedProfile) {
- const deadline = Date.now() + 12 * 60_000;
- // The Directory is the discovery surface. Pass its verified listing
- // pointer to the Butler; the gateway independently dereferences and
- // verifies the signed DACS-1 artifact before negotiation.
- const request: Record = { profileId: selectedProfile.id, ...parsed, paymentRail: selectedPaymentRail };
- if (selectedProfile.id === "security-audit-rfq") try {
- const { response: catalog, body } = await fetchJsonBeforeDeadline<{ listings?: Array<{ listingId?: string; anchor?: { locator?: string }; offering?: { title?: string; negotiation?: string[] } }> }>(`/api/dacs/listings?rail=${encodeURIComponent(selectedPaymentRail)}&limit=100`, { signal: controller.signal }, deadline);
- if (catalog.ok) {
- const auditor = body.listings?.find((item) => item.listingId === "audit-negotiator" && item.offering?.negotiation?.includes("rfq") && /auditor/i.test(item.offering?.title ?? ""));
- const ref = auditor?.anchor?.locator;
- if (ref) request.auditorListingRef = ref;
- }
- } catch { /* gateway will derive and verify the configured Auditor slot */ }
- // One key per intentional run; a retry of this same run reuses it so
- // the gateway dedupes to the existing job (never a second payment).
- procurementRunId.current ??= crypto.randomUUID();
- // Read-record → write-record → POST runs under a cross-tab exclusive
- // lock, with the record RE-READ from storage inside the section: two
- // tabs can otherwise interleave, the loser overwriting the winner's
- // record and then deleting it on its own 4xx.
- const started = await withExclusiveProcurementLock(browserLocks(), controller.signal, async (): Promise => {
- const existing = readStoredRun();
- if (existing && existing.runId !== procurementRunId.current) {
- setPhase("ready");
- setError("Another procurement run's recovery record is active (possibly from another tab). Use “Check & resume” in the banner — or dismiss it once reconciled — before starting a new purchase.");
- return null;
- }
- // Persist the key + input BEFORE dispatch: a reload/crash between
- // the POST and the job-id response must not lose the only handle on
- // a purchase the gateway may have started. Restored on mount.
- const runRecord: StoredProcurementRun = { runId: procurementRunId.current!, goal, input: request, startedAt: new Date().toISOString() };
- if (!updateStoredRun(runRecord)) {
- // No durable recovery record → a reload would lose the only
- // handle on the purchase. Refuse to send the paid request at all.
- setPhase("ready");
- setError("This browser cannot durably save the purchase-recovery record (storage is blocked or full), so the paid request was NOT sent. Free up site storage or use a different browser profile, then run again.");
- return null;
- }
- const { response: start, body: startBody } = await fetchJsonBeforeDeadline(`${BUTLER}/demo/procurement`, {
- method: "POST",
- headers: { "content-type": "application/json", "Idempotency-Key": runRecord.runId },
- body: JSON.stringify(request), signal: controller.signal,
- }, deadline);
- if (!start.ok) {
- // A 4xx is a verified pre-job rejection (nothing was created), so
- // THIS run's record has nothing to protect — release it only if
- // it is still ours. A 5xx/opaque failure keeps it: the job may
- // exist, and the key is the only handle.
- if (start.status >= 400 && start.status < 500) releaseStoredRun(runRecord.runId);
- if (fieldMappedRejection(startBody)) return "handled";
- throw new Error(message(startBody));
- }
- const job = parseProcurementJob(startBody);
- procurementJobRef.current = job; // synchronous — the catch relies on it
- updateStoredRun({ ...runRecord, jobId: job.id });
- setProcurementJob(job);
- return job;
- }).catch((cause: unknown) => {
- if (cause instanceof ProcurementLockUnavailableError) {
- setPhase("ready");
- setError(`${cause.message} Use a current version of Chrome, Edge, Firefox or Safari — the paid request was NOT sent.`);
- return null;
- }
- throw cause;
- });
- if (started === null || started === "handled") return;
- await followProcurementJob(started, controller, deadline);
- return;
- }
- const { response: res, body } = await fetchJsonWithTimeout(`${BUTLER}/demo/butler`, {
- method: "POST", headers: { "content-type": "application/json" },
- body: JSON.stringify({ goal, agent: plan.butler.selectedAgent, input: parsed }),
- signal: controller.signal,
- }, AGENT_TIMEOUT_MS, AGENT_TIMEOUT_MESSAGE);
- if (!res.ok) {
- if (fieldMappedRejection(body)) return;
- throw new Error(message(body));
- }
- const completed = parseButlerRun(body);
- setResult(completed); setPhase("done");
- if (completed.outputAttestation) {
- setReceipt(completed.outputAttestation);
- void watchReceipt(completed.outputAttestation);
- }
- } catch (cause) {
- const cancelled = (cause as Error).name === "AbortError";
- const isProcurement = isProcurementSel;
- const haveJob = procurementJobRef.current !== null;
- // The retry-reuses-the-key reassurance only holds while the key is
- // retained — a terminal failed job is explained by the recovery box
- // instead, so its gateway reason is shown verbatim.
- const failedJob = (procurementJobRef.current as ProcurementJob | null)?.status === "failed";
- setError(cancelled
- ? isProcurement && haveJob
- ? "Stopped watching in this browser. The gateway is still completing this procurement job — it was NOT cancelled, and any payment it makes still happens. Resume below to keep following the same job; no second purchase will be started."
- : "Run cancelled in this browser. No result was accepted; you can retry the same bounded job."
- : isProcurement && !failedJob
- ? `${(cause as Error).message} — Retrying reuses the same idempotency key, so the gateway resumes the existing job instead of starting a second paid purchase.`
- : (cause as Error).message);
- setPhase("error");
- } finally {
- if (runAbort.current === controller) runAbort.current = null;
- }
- }
-
- /**
- * Follow an already-started procurement job to completion. Used by the
- * initial run and by "Resume status" after the user stops watching — the
- * job id is retained so resuming NEVER creates a second job or payment.
- */
- async function followProcurementJob(initial: ProcurementJob, controller: AbortController, deadline: number) {
- let current = initial;
- while (current.status === "running" && Date.now() < deadline) {
- await waitWithSignal(Math.min(2_000, Math.max(0, deadline - Date.now())), controller.signal);
- const { response: poll, body: pollBody } = await fetchJsonBeforeDeadline(`${BUTLER}/demo/procurement/${encodeURIComponent(current.id)}`, { signal: controller.signal }, deadline);
- if (!poll.ok) throw new Error(message(pollBody));
- current = parseProcurementJob(pollBody);
- // Keep the synchronous ref aligned with the rendered state. The outer
- // catch uses it before React effects run to distinguish a terminal
- // gateway failure from a transient watcher error.
- procurementJobRef.current = current;
- setProcurementJob(current);
- }
- if (current.status === "failed") {
- // Only the gateway's explicit failedBeforePayment=true proves no money
- // moved — then (and only then) a fresh purchase is safe, so the key and
- // persisted record are released (if the record is still this run's).
- // Anything else (flag false or absent) may have paid: keep both, so the
- // job stays referenced for the gateway's operator recovery path and no
- // new purchase is offered.
- if (current.failedBeforePayment === true) {
- const ownedRunId = procurementRunId.current;
- procurementRunId.current = null;
- releaseStoredRun(ownedRunId);
- }
- throw new Error(current.error ?? "the full procurement flow failed safely");
- }
- if (current.status !== "complete") throw new Error("the full procurement flow is still running; use Resume status to keep following it");
- setResult(current.result); setPhase("done");
- const ownedRunId = procurementRunId.current;
- procurementRunId.current = null;
- releaseStoredRun(ownedRunId);
- }
-
- /** Resume following the existing procurement job (never a new purchase). */
- async function resumeProcurement() {
- const job = procurementJobRef.current;
- if (!job) return;
- runAbort.current?.abort();
- const controller = new AbortController();
- runAbort.current = controller;
- setPhase("running"); setError("");
- setRunStartedAt(Date.now()); setElapsedMs(0);
- try {
- await followProcurementJob(job, controller, Date.now() + 12 * 60_000);
- } catch (cause) {
- const cancelled = (cause as Error).name === "AbortError";
- setError(cancelled
- ? "Stopped watching in this browser. The gateway is still completing this procurement job — resume below to keep following it; no second purchase will be started."
- : (cause as Error).message);
- setPhase("error");
- } finally {
- if (runAbort.current === controller) runAbort.current = null;
- }
- }
-
- /**
- * Reconcile a persisted run after a reload. With a job id this only reads
- * status. Without one (the reload raced the POST response) it re-POSTs the
- * SAME stored input under the SAME idempotency key, so the gateway returns
- * the original job if it exists — it can never start a second purchase for
- * this run.
- */
- async function resumeStoredRun() {
- const stored = storedRun;
- if (!stored) return;
- runAbort.current?.abort(); receiptAbort.current?.abort();
- const controller = new AbortController();
- runAbort.current = controller;
- procurementRunId.current = stored.runId;
- setGoal(stored.goal);
- const storedProfileId = typeof stored.input.profileId === "string" ? stored.input.profileId : "security-audit-rfq";
- const storedAgent = PROFILE_AGENT[storedProfileId] ?? PROFILE_AGENT["security-audit-rfq"]!;
- const storedPaymentRail: PaymentRail = stored.input.paymentRail === "pay-x402" ? "pay-x402" : "pay-dem";
- setSelectedProfileId(storedProfileId);
- setSelectedPaymentRail(storedPaymentRail);
- setPlan({
- butler: {
- selectedAgent: storedAgent.name,
- label: storedAgent.label,
- selectionEngine: "restored run",
- rationale: "Reconnecting to the procurement run this browser dispatched earlier — same idempotency key, so no second purchase can start.",
- alternatives: [],
- },
- proposedInput: stored.input,
- inputNote: "Restored from this browser's persisted run record.",
- });
- const { profileId: _profileId, auditorListingRef: _auditorListingRef, ...storedFormInput } = stored.input;
- setInputValue(storedFormInput);
- setResult(undefined); setReceipt(null); setReceiptMessage(""); setGatewayFieldErrors({});
- setProcurementJob(null); procurementJobRef.current = null;
- setSubmittedSummary(summarizeAgentInput(storedAgent.name, storedFormInput));
- setPhase("running"); setError("");
- setRunStartedAt(Date.now()); setElapsedMs(0);
- const deadline = Date.now() + 12 * 60_000;
- try {
- let job: ProcurementJob;
- if (stored.jobId) {
- const { response, body } = await fetchJsonBeforeDeadline(`${BUTLER}/demo/procurement/${encodeURIComponent(stored.jobId)}`, { signal: controller.signal }, deadline);
- if (!response.ok) throw new Error(message(body));
- job = parseProcurementJob(body);
- } else {
- // The reload raced the original POST response: re-POST the SAME
- // stored input under the SAME key — inside the cross-tab lock, like
- // every paid dispatch, and refusing without a real lock manager.
- // The lock only SERIALIZES: while this section was queued, another
- // tab may have dismissed this record and started a new run, so the
- // section re-validates its captured record against storage NOW and
- // must never act on (or overwrite) a record it no longer owns.
- job = await withExclusiveProcurementLock(browserLocks(), controller.signal, async () => {
- const decision = resumeDispatchDecision(stored, readStoredRun());
- if (decision.action === "abort-stale") throw new StaleResumeRecordError();
- if (decision.action === "read") {
- // Another tab already learned this run's job id — read it, never re-POST.
- const { response, body } = await fetchJsonBeforeDeadline(`${BUTLER}/demo/procurement/${encodeURIComponent(decision.jobId)}`, { signal: controller.signal }, deadline);
- if (!response.ok) throw new Error(message(body));
- return parseProcurementJob(body);
- }
- const { response, body } = await fetchJsonBeforeDeadline(`${BUTLER}/demo/procurement`, {
- method: "POST",
- headers: { "content-type": "application/json", "Idempotency-Key": stored.runId },
- body: JSON.stringify(stored.input), signal: controller.signal,
- }, deadline);
- if (!response.ok) {
- if (response.status >= 400 && response.status < 500) releaseStoredRun(stored.runId);
- throw new Error(message(body));
- }
- const parsedJob = parseProcurementJob(body);
- updateStoredRun({ ...stored, jobId: parsedJob.id });
- return parsedJob;
- });
- }
- procurementJobRef.current = job;
- setProcurementJob(job);
- await followProcurementJob(job, controller, deadline);
- } catch (cause) {
- if (cause instanceof StaleResumeRecordError) {
- // The record now belongs to another run (or is gone); this tab must
- // not keep its key or offer procurement retries against it.
- procurementRunId.current = null;
- setPlan(null); setPhase("idle");
- setError(cause.message);
- return;
- }
- const cancelled = (cause as Error).name === "AbortError";
- const failedJob = (procurementJobRef.current as ProcurementJob | null)?.status === "failed";
- setError(cancelled
- ? "Stopped watching in this browser. Any live gateway job continues — resume again to keep following it."
- : cause instanceof ProcurementLockUnavailableError
- ? `${cause.message} Use a current version of Chrome, Edge, Firefox or Safari — no request was sent.`
- : failedJob
- ? (cause as Error).message
- : `${(cause as Error).message} — Resuming again reuses the same idempotency key, so no second purchase can start.`);
- setPhase("error");
- } finally {
- if (runAbort.current === controller) runAbort.current = null;
- }
- }
-
- function selectAgent(agent: AgentCard) {
- runAbort.current?.abort(); receiptAbort.current?.abort();
- setGoal(agent.exampleGoal);
- const profile = profiles.find((candidate) => procurementProfileCard(candidate, defaultPaymentRail(candidate)).name === agent.name);
- const paymentRail = profile ? defaultPaymentRail(profile) : "pay-dem";
- const railAgent = profile ? procurementProfileCard(profile, paymentRail) : agent;
- setSelectedProfileId(profile?.id ?? null);
- setSelectedPaymentRail(paymentRail);
- setPlan({
- butler: {
- selectedAgent: agent.name,
- label: agent.label,
- selectionEngine: "user-selected test",
- rationale: `You selected ${agent.label}. Fill in the job below (or load its example), and I will supervise the run and show you the evidence it returns.`,
- alternatives: [],
- },
- proposedInput: railAgent.exampleInput,
- inputNote: "Your values are submitted unchanged to the live gateway, which applies specialist validation before execution.",
- });
- // Switching agents resets to that agent's blank form. Examples are never
- // submitted silently — "Load example" below is the only way they enter.
- // Schema-driven agents seed select/checkbox defaults so the form's display
- // matches submitted state.
- const schema = hasBuiltinForm(railAgent.name) ? null : parseAgentFieldSchema(railAgent);
- setInputValue(schema ? initialSchemaInput(schema) : initialAgentInput(railAgent.name, paymentRail));
- procurementRunId.current = null;
- setGatewayFieldErrors({}); setSubmittedSummary([]);
- setResult(undefined); setProcurementJob(null); setReceipt(null); setReceiptMessage(""); setPhase("ready"); setError("");
- window.scrollTo({ top: 0, behavior: "smooth" });
- }
-
- function selectPaymentRail(rail: PaymentRail) {
- if (!selectedProfile || rail === selectedPaymentRail || !selectedProfile.railReadiness[rail]?.executable) return;
- const railAgent = procurementProfileCard(selectedProfile, rail);
- setSelectedPaymentRail(rail);
- setPlan((current) => current ? { ...current, proposedInput: railAgent.exampleInput } : current);
- const schema = hasBuiltinForm(railAgent.name) ? null : parseAgentFieldSchema(railAgent);
- setInputValue(schema ? initialSchemaInput(schema) : initialAgentInput(railAgent.name, rail));
- procurementRunId.current = null;
- setGatewayFieldErrors({}); setSubmittedSummary([]); setError("");
- }
-
- function loadExample() {
- if (!selected) return;
- procurementRunId.current = null;
- setInputValue(JSON.parse(JSON.stringify(selected.exampleInput)) as Record);
- setGatewayFieldErrors({});
- setError("");
- }
-
- function editInput(next: Record) {
- setInputValue(next);
- // Edited input is a new intent: a fresh idempotency key so a retry can't
- // dedupe to a job that ran the previous payload.
- procurementRunId.current = null;
- // Stale gateway verdicts don't apply to edited input.
- if (Object.keys(gatewayFieldErrors).length) setGatewayFieldErrors({});
- }
-
- return (
-
-
-
- LIVE DACS WALKTHROUGH
-
Buy agent work. Watch the deal.
-
-
Choose a real procurement route and how to pay: native DEM on Demos, or USDC through x402 on Base Sepolia. The Butler verifies the complete deal and exposes every receipt as it happens.
-
-
- {/* Suppress the banner only when THIS tab is already tracking the
- record's job. A record with no jobId (the reload-raced-the-POST
- case) must always surface — that is the exact state it protects. */}
- {storedRun && phase !== "running" && !(procurementJob && storedRun.jobId && procurementJob.id === storedRun.jobId) && (
-
-
- A procurement run from this browser is still on record
-
Started {new Date(storedRun.startedAt).toLocaleString()}{storedRun.jobId ? ` · job ${compact(storedRun.jobId, 8, 6)}` : " · the job id was never received"}. Resuming reconnects under the same idempotency key, so it can never start a second purchase.
-
-
-
-
-
-
- )}
-
-
-
-
B
DACS Butler{agents.length ? `${agents.length} procurement routes live · gateway connected` : "Connecting to the procurement gateway…"}
-
-
Butler
Welcome. Choose how you want to buy agent work. I’ll run the complete live DACS deal and explain the evidence produced at every phase.
- {goal && phase !== "idle" &&
You
{goal}
}
- {plan &&
Butler
{plan.butler.label} is ready. {plan.butler.rationale}
{plan.butler.selectionEngine}
}
- {phase === "done" &&
Butler
The specialist finished. Its result is ready now; the separate receipt status below shows any on-chain anchoring still completing in the background.
Procurement failed before any paymentThe gateway confirms no money moved (its reason is shown above). Retrying starts a fresh purchase attempt with a new idempotency key.
Procurement failed after payment may have moved — do not re-purchaseJob {procurementJob.id} is preserved on the gateway with its payment evidence; the gateway cannot confirm the failure happened before payment, so starting a new run could pay twice. A gateway operator can recover the delivery for the existing job without another payment.
Watching stopped — the procurement job is still live on the gatewayJob {procurementJob.id} was not cancelled; resuming follows the same job and never starts a second purchase.
-
- >
- ) : isProcurementSel ? (
- <>
-
Procurement stopped safelyRetrying reuses this run’s idempotency key, so the gateway resumes the existing job rather than starting a second paid purchase.
-
- >
- ) : (
- <>
-
{plan.butler.label} stopped safelyYour entered job details are still available.
- {selectedRailReadiness.railGovernance.conformantAuthority ? "Canonical rail authority" : "Operator-provisional rail authority"}{selectedRailReadiness.railGovernance.conformantAuthority ? "Authority conforms to the current DACS rail registry rules." : "Usable in this live demo while the standardised rail authority is being agreed."}
- Read disclosure ↗
-
}
-
- Hosted demo buyer · automatic executionThe gateway runs every phase server-side with its demo identity and wallets. It does not pause for confirmations or accept your own DACS identity or payment signer.
-
{procurementWaiting ? "Your purchase is safely queued behind the active buyer-wallet deal. No payment has started yet." : procurementPreview ? "The result is available below. Keep this page open while both mandatory bundle copies reconcile." : "Keep this page open — settlement and chain confirmations appear here live."}
{specialistDurationMs !== undefined && Specialist completed in {elapsedLabel(specialistDurationMs)} · {paymentRailLabel(selectedPaymentRail)}}
}
-
-
THREE WAYS TO PROCURE
Production agents, DEM or x402, full DACS
Each route uses the gateway’s rail-specific live schema and runs Identify → Vet → Negotiate → Settle → Verify. Sealed tender stays hidden until its DACS-3 role model is released.