Skip to content

Redesign DACS directory for human and agent discovery#8

Merged
randomblocker merged 5 commits into
mainfrom
agent/dacs-directory-ux
Jul 10, 2026
Merged

Redesign DACS directory for human and agent discovery#8
randomblocker merged 5 commits into
mainfrom
agent/dacs-directory-ux

Conversation

@randomblocker

@randomblocker randomblocker commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What changed

  • redesigns discovery around services, with a clearer Demos-aligned promise, real catalog stats, durable URL-backed filters, decision-focused cards, seller-scoped service pages, and explicit human/JSON actions
  • adds responsive navigation, mobile layouts, responsive ledgers, focus management, semantic form controls, reduced-motion support, and complete empty/error states
  • rebuilds seller, registration, deal, and verification journeys around progressive disclosure and plain-language trust states
  • adds an interactive identify → vet → negotiate → settle → verify explainer modeled on demos.network's product storytelling
  • makes the directory discoverable to agents from the origin via well-known manifests, an API index, OpenAPI 3.1, JSON Schema, llms.txt, typed Link headers, JSON alternates, ETags, cache policy, robots, and a live-catalog sitemap

Why

The merged MVP proved the cryptographic and catalog behavior, but the interface still read like an operator console. Human buyers need to understand the offer and trust model quickly, while agent buyers need a first-class machine contract that never depends on scraping HTML.

Impact

Humans can now find a service, understand payment/delivery and three separate trust layers, inspect the seller, and reach the truthful next action. Agents can start with only the site origin and discover the catalog, filters, schemas, listing artifacts, seller data, and freshness state.

Deployments should set NEXT_PUBLIC_DIRECTORY_URL to the public origin so canonical URLs, sitemap, and plain-text discovery links use the production host.

Railway deployment

  • live preview: https://community-production-9ab1.up.railway.app
  • Railpack build and /api/health readiness check pass
  • persistent catalog volume mounted at /data
  • catalog refresh loop runs every 15 minutes and currently exposes 9 active listings
  • public manifests, OpenAPI, JSON links, sitemap, and response Link headers resolve to the Railway domain

GitHub-source deployments now clone the pinned private SDK with a fine-grained read-only token stored as a Railway secret. The credential is supplied through Git environment configuration and is not written to the checkout or remote URL. The service is configured with the directory monorepo root and its checked-in railway.json; the CLI packaging path remains available as a fallback.

Validation

  • npm test — 22/22 passing
  • npm run typecheck
  • npm run build — all routes compile
  • npm audit --omit=dev — 0 vulnerabilities
  • git diff --check
  • production HTTP smoke test across the homepage, manifests, API index, listings, OpenAPI, schema, robots, sitemap, registration, verification, and conditional GET behavior
  • clean Railway GitHub-source build at 9c008bb, including authenticated private-SDK clone, Railpack build, health check, persistent indexed data, public-origin links, and 9 active listings

Visual QA note

Source, responsive behavior, semantic structure, production rendering, and HTTP routes were validated. An interactive browser session was unavailable in this workspace, so desktop/mobile screenshot comparison and direct wallet interaction should be included in preview-deployment review.

@railway-app railway-app Bot temporarily deployed to affectionate-upliftment / production July 10, 2026 18:54 Inactive
Addresses the four findings from the PR #8 review:

- structuredData: new safeJsonLd() escapes `<` when serializing JSON-LD.
  JSON.stringify does not escape `<`, so seller-controlled listing
  titles/descriptions and display names containing `</script>` broke out of
  the homepage and service-page ld+json blocks — a stored XSS reachable by
  any seller the passive scanner discovers. Data is unchanged after
  JSON.parse; a regression test proves both properties.
- DirectoryExplorer/service hero: restore the owner-registered /
  found on-chain / unverified-submission provenance cue that PR #7 added and
  this redesign dropped. Without it an unsigned third-party submission's
  chosen display name rendered unqualified on cards and the service hero.
- publicUrl: log a one-time production warning when NEXT_PUBLIC_DIRECTORY_URL
  is unset, and document the failure mode in the README — otherwise canonical
  URLs, the sitemap, robots and llms.txt silently advertise localhost:3400.
- sitemap: only advertise active listings, matching the discovery surface
  (revoked listings remain on seller history pages but are not destinations).
- contracts: OpenAPI limit maximum corrected 100 → 200 to match what
  parsePagination actually enforces; contract test locks the two together.

typecheck clean, 23/23 tests, production build passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@randomblocker

Copy link
Copy Markdown
Contributor Author

Reviewed this PR and pushed fixes for the findings in 1a62615 (rebased onto the deployment commits that landed mid-review). What changed and why:

1. Stored XSS through the JSON-LD blocks — fixed (the one blocker)

app/page.tsx and the service page serialized seller-controlled data (listing title/description, display name) into <script type="application/ld+json"> via plain JSON.stringify, which escapes quotes but not <. A listing whose title contains </script><img src=x onerror=…> therefore terminated the script element and injected live markup. A listing signature proves ownership, not content safety — and the passive chain scanner auto-discovers listings, so no registration was needed to plant this. The homepage rendered JSON-LD for up to 100 listings, so one malicious listing executed for every visitor.

Fix: new src/components/structuredData.ts exports safeJsonLd(), which escapes every < as < — closing </script> and <!-- breakouts while JSON.parse still yields byte-identical data for search-engine consumers. Both sinks now use it, and a regression test asserts both properties (no < in output, data round-trips). These were the only two dangerouslySetInnerHTML sinks with dynamic content; everything else renders through React's escaping.

2. Provenance cue restored on cards and the service hero

The redesign dropped the "owner-registered / found on-chain / unverified submission" label that PR #7 added, leaving it only on the seller profile. That mattered because an unsigned third-party registration controls the display name — the redesigned cards showed "by {name}" with a "listing verified" badge right next to it, lending unearned credibility to a name nobody attested. The cue is back on directory cards (byline-src, reusing the CSS that survived the redesign) and on the service hero (with a tooltip explaining exactly what is and isn't attested).

3. Localhost canonical-URL footgun — now loud instead of silent

sitemap.ts, robots.ts, llms.txt, and metadataBase fall back to http://localhost:3400 when NEXT_PUBLIC_DIRECTORY_URL is unset — meaning a prod deployment that forgets the env var silently publishes localhost links in its sitemap and canonical tags. directoryBaseUrl() now logs a one-time warning in production, and the README env table states the failure mode, not just the default. (Kept as a warning rather than a hard failure so local next start keeps working.)

4. Discovery consistency nits

  • sitemap.ts now filters to status === "active" — revoked listings stay browsable on seller history pages but are no longer advertised as sitemap destinations, matching what /api/dacs/listings exposes.
  • OpenAPI declared limit max 100 while parsePagination enforces 200; corrected to 200 and added a contract test that derives the assertion from the OpenAPI document itself, so the two can't drift apart again.

Not changed, deliberately: the JSON-LD offers block keys off pricing.priceHint, which the indexer never populates yet — it's dead code today but becomes live the moment pricing lands, so I left it.

Validation on the rebased branch: npm run typecheck clean, 24/24 tests, production build compiles all 23 routes. The rest of the PR — the well-known manifests, OpenAPI/schema surface, ETag/conditional-GET handling, and the rewritten register flow (wallet signing path intact) — reviewed clean.

@railway-app railway-app Bot temporarily deployed to affectionate-upliftment / production July 10, 2026 19:43 Inactive
@randomblocker randomblocker marked this pull request as ready for review July 10, 2026 20:01
@randomblocker randomblocker merged commit 22fb070 into main Jul 10, 2026
1 check passed
@mj-deving

Copy link
Copy Markdown
Contributor

Ran a post-merge Railway preview QA pass on https://community-production-9ab1.up.railway.app.

Finding: the /verify happy-path demo gets as far as loading live-x402-1783421389437, but clicking Verify this deal in your browser ends in:

Could not verify
The chain records could not be reached. Check the network and try again.

Why it matters: the preview now makes the Directory and deal lookup tangible, but the proof CTA currently terminates in an error for the sample deal. This looks like a runtime verifier/RPC/artifact reachability issue rather than layout: no browser console errors, /api/dacs/deal-owners?jobId=live-x402-1783421389437 returned 200, and the referenced /api/dacs/artifact?... requests returned 200 during the run.

Other checks from the same pass:

  • desktop and mobile rendered /, ReviewBot service detail, /verify, and /register with no console errors and no horizontal overflow
  • discovery path resolves on the Railway origin: well-known manifest -> API index -> listings -> listing schema -> signed listing artifact path
  • core HTTP smoke returned 200 for /api/health, /.well-known/dacs-directory.json, /api/dacs, /api/dacs/listings, /openapi.json, /llms.txt, /robots.txt, and /sitemap.xml
  • GitHub currently shows the Railway context as failure for this PR even though the preview origin is serving the checked routes
  • PR test: add Directory seed smoke #9 only overlaps README/package with this merge; merge-tree showed no conflict markers

Suggested fix: re-check the live verifier path used after the deal bundle is loaded, especially any RPC/base URL/env wiring that differs between local and Railway. The UI shell itself looked good in this pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants