diff --git a/dashboard/app/docs/api/page.tsx b/dashboard/app/docs/api/page.tsx
index 43c46de..754499f 100644
--- a/dashboard/app/docs/api/page.tsx
+++ b/dashboard/app/docs/api/page.tsx
@@ -16,6 +16,7 @@ import {
import { CommandBlock } from '@/components/copy';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/api' },
title: 'HTTP API',
description:
'Reference for every AgentGate HTTP endpoint: the 402 paywall gateway (health, public metadata, the paywalled proxy, self-service upstream mapping, and the admin API) and the dashboard read-only /api routes.',
@@ -793,8 +794,8 @@ export default function Page() {
>
diff --git a/dashboard/app/docs/architecture/page.tsx b/dashboard/app/docs/architecture/page.tsx
index 46dbf67..069173b 100644
--- a/dashboard/app/docs/architecture/page.tsx
+++ b/dashboard/app/docs/architecture/page.tsx
@@ -13,6 +13,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/architecture' },
title: 'Architecture',
description:
'How AgentGate fits together: the monorepo packages and their roles, the ChainClient seam that swaps mock and live backends, the 402 reverse-proxy data flow, where state lives, and the tech stack.',
@@ -159,8 +160,9 @@ export default function ArchitecturePage() {
There are exactly two implementations of that interface, both in{' '}
packages/chain. Which one is constructed is selected at startup by{' '}
- AGENTGATE_MODE (default mock); the gateway, CLI, buyer agent, and dashboard all
- receive an injected ChainClient and are identical regardless of backend.
+ AGENTGATE_MODE; the gateway, buyer agent, and dashboard default to mock via{' '}
+ loadConfig(), while the published CLI defaults to live. All of them receive an
+ injected ChainClient and are identical regardless of backend.
fetchPaid returns a PayAndFetchResult (status, body,{' '}
- paid, and on payment invoice / deployHash / priceMotes). A
+ paid, and on payment requirements / deployHash / priceMotes /{' '}
+ settlement). A
first response that is not a 402 passes straight through with paid: false. For every
option, field and pending-retry detail, see the{' '}
Client SDK reference.
diff --git a/dashboard/app/docs/changelog/page.tsx b/dashboard/app/docs/changelog/page.tsx
new file mode 100644
index 0000000..1b3d381
--- /dev/null
+++ b/dashboard/app/docs/changelog/page.tsx
@@ -0,0 +1,81 @@
+import { docMeta } from '@/lib/seo';
+import { DOCS_VERSION, REGISTRY_HASH_SHORT } from '@/lib/version';
+import { DocHeader, H2, M, P, DocLink, NextLinks } from '@/components/docs';
+
+export const metadata = docMeta(
+ '/docs/changelog',
+ 'Changelog',
+ 'Notable changes to AgentGate — the CLI, gateway, smart contracts and docs — newest first.',
+);
+
+export default function Page() {
+ return (
+ <>
+
+ Notable changes to AgentGate, newest first. Current: CLI v{DOCS_VERSION.cli} · SDK v
+ {DOCS_VERSION.sdk} · registry {REGISTRY_HASH_SHORT} on {DOCS_VERSION.network}.
+ >
+ }
+ />
+
+
+ Self-service gateway mapping.wrap now maps
+ the upstream with an owner-signed request to /services/<id>/map — a live wrap
+ needs only --pem, no admin token.
+
+
+ Role-oriented docs. The sidebar and overview map
+ are organized by role (For sellers / For buyers / Run a gateway) from a single source of
+ truth.
+
+
+ Docs accessibility + accuracy pass. Skip-to-content
+ link, keyboard focus rings, AA-contrast parameter tables, copy buttons on every code block,
+ a CLI config-flags reference, a corrected status no-key example, and fixed
+ cross-links.
+
+
+ Discovery. Added a sitemap, robots, canonical URLs,
+ OpenGraph/Twitter cards, JSON-LD, in-docs ⌘K search, and this changelog.
+
+
+
+
2026-06-30 — CLI published to npm
+
+
+ @mdlog/agentgate v{DOCS_VERSION.cli} published — run it with{' '}
+ npx @mdlog/agentgate, no install or clone.
+
+
+ The published CLI defaults to live mode and the deployed registry hash, so{' '}
+ list and status read Casper Testnet with no configuration. See{' '}
+ CLI.
+
Hosted gateway and dashboard brought online so agents can transact without local setup.
+
+
+
+ >
+ );
+}
diff --git a/dashboard/app/docs/cli/page.tsx b/dashboard/app/docs/cli/page.tsx
index 9134caa..72e67fe 100644
--- a/dashboard/app/docs/cli/page.tsx
+++ b/dashboard/app/docs/cli/page.tsx
@@ -15,6 +15,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/cli' },
title: 'CLI',
description:
'Reference for the agentgate CLI: wrap, list, status, pause, resume and demo-accounts. Every flag, argument, default and example output, verified against packages/cli/src/bin.ts.',
@@ -158,6 +159,49 @@ export default function Page() {
]}
/>
+
Config flags
+
+ Each of these overrides the matching environment variable (flag > env > default).{' '}
+ --mode, --node-url and --registry are accepted by all six commands; the
+ signer and key flags are read only by the commands that need them.
+
+ --mode {''}, AGENTGATE_MODE, live, 'all commands'],
+ [--node-url {''}, CASPER_NODE_URL, 'Casper Testnet', 'all commands'],
+ [
+ --registry {''},
+ REGISTRY_CONTRACT_PACKAGE_HASH,
+ 'the deployed package hash',
+ 'all commands',
+ ],
+ [
+ --pem {''},
+ SELLER_SIGNER_PEM_PATH,
+ (none),
+ 'wrap, pause, resume (live writes)',
+ ],
+ [
+ --admin-token {''},
+ AGENTGATE_ADMIN_TOKEN,
+ dev-admin-token,
+ 'wrap (mock / self-hosted admin)',
+ ],
+ [
+ --api-key {''},
+ CSPR_CLOUD_API_KEY,
+ (none),
+ 'status (attestation history)',
+ ],
+ ]}
+ />
+
+ --pem is a path (safe), but --admin-token and --api-key put the secret
+ itself into shell history and ps output. Prefer the environment variable for those two
+ in any shared or CI environment.
+
+
{/* ───────────────────────────── wrap ───────────────────────────── */}
+ wrap also reads the config flags — most
+ importantly --pem (required for live writes) and --admin-token (mock or
+ self-hosted admin mapping).
+
Example
-
+ {''},
'yes',
'Positive integer (1-based). A non-numeric or < 1 value throws INVALID_SERVICE_ID; an id with no on-chain record throws SERVICE_NOT_FOUND.',
],
+ [
+ --api-key {''},
+ 'no',
+ 'CSPR.cloud key used only to fetch attestation history (overrides CSPR_CLOUD_API_KEY). Without it, the record, score and tier still print but attestation history is skipped.',
+ ],
]}
/>
',
'owner: 01',
'attestor: 01',
+ 'trust: reliable (9/10 calls ok)',
+ 'attestations: (set CSPR_CLOUD_API_KEY or pass --api-key to view history)',
+ ].join('\n')}
+ />
+
+ The record, score and trust tier always print with no keys. Attestation history is the one
+ part that needs a CSPR.cloud key: without{' '}
+ --api-key (or CSPR_CLOUD_API_KEY) the last line is the hint above and the
+ command stops. Pass the key to fetch the newest attestations:
+
+
At most 10 attestations are shown (STATUS_ATTESTATION_LIMIT), newest first, each
- prefixed [ok ] or [FAIL]. With none, it prints{' '}
- attestations: none yet and stops. An inactive service is tagged{' '}
- [INACTIVE] after its name, and the description: line is omitted when the
- description is empty.
+ prefixed [ok ] or [FAIL]; with a key but no attestations yet it prints{' '}
+ attestations: none yet. An inactive service is tagged [INACTIVE] after its
+ name, and the description: line is omitted when the description is empty.
Like wrap, pause resolves the owner signer from{' '}
- MOCK_SELLER_ACCOUNT (mock) or SELLER_SIGNER_PEM_PATH (live). If the
+ MOCK_SELLER_ACCOUNT (mock) or SELLER_SIGNER_PEM_PATH / --pem (live). If the
signing key is not the service owner, the chain's 403 is reworded into a clear
owner-only not_authorized error.
diff --git a/dashboard/app/docs/configuration/page.tsx b/dashboard/app/docs/configuration/page.tsx
index b812f7d..df1d41d 100644
--- a/dashboard/app/docs/configuration/page.tsx
+++ b/dashboard/app/docs/configuration/page.tsx
@@ -13,6 +13,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/configuration' },
title: 'Configuration',
description:
'Complete reference for every AgentGate environment variable: names, defaults, validation rules, which are required in live mode, and the loadConfig() guardrails that refuse an unsafe live configuration.',
@@ -498,7 +499,7 @@ export default function ConfigurationPage() {
Any CSPR value anywhere in the system (prices, budgets) allows at most 9 decimal places — 1
mote = 1e-9 CSPR — and is processed as a bigint-backed decimal string. See{' '}
- Protocol → Money units.
+ Protocol → the 402 invoice body.
The deployed registry's hash-<64hex>. Empty is accepted by{' '}
loadConfig() but every registry read/write then fails — see{' '}
- Contract → Deploy status.
+ Contract → Build and deploy.
,
],
[
diff --git a/dashboard/app/docs/errors/page.tsx b/dashboard/app/docs/errors/page.tsx
index c89acfc..4651e71 100644
--- a/dashboard/app/docs/errors/page.tsx
+++ b/dashboard/app/docs/errors/page.tsx
@@ -13,6 +13,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/errors' },
title: 'Error codes',
description:
'Every AgentGate error code: the AgentGateError(code, message, httpStatus) shape, the lowercased JSON error body, and a code-by-code reference grouped by config, chain/live, the 402 paywall + admin API, the client SDK, the buyer agent, and the CLI.',
diff --git a/dashboard/app/docs/installation/page.tsx b/dashboard/app/docs/installation/page.tsx
index e03044a..55c766f 100644
--- a/dashboard/app/docs/installation/page.tsx
+++ b/dashboard/app/docs/installation/page.tsx
@@ -14,6 +14,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/installation' },
title: 'Installation',
description:
'Requirements, clone-and-install, the workspace layout, mock vs live mode, the environment file, and how to verify your install passes tests, typecheck and the one-shot demo.',
@@ -188,9 +189,10 @@ export default function InstallationPage() {
Modes — mock vs live
- The AGENTGATE_MODE environment variable (default mock) selects the chain
- backend behind the ChainClient seam. Everything above that seam — the gateway, CLI,
- buyer agent and dashboard — is byte-for-byte identical in both modes.
+ The AGENTGATE_MODE environment variable selects the chain backend behind the{' '}
+ ChainClient seam. The stack (gateway, buyer agent, dashboard) defaults to mock via{' '}
+ loadConfig(); the published npx CLI defaults to live. Everything above that
+ seam — the gateway, CLI, buyer agent and dashboard — is byte-for-byte identical in both modes.
+
+
+
diff --git a/dashboard/app/docs/page.tsx b/dashboard/app/docs/page.tsx
index 7b9d0c1..0dd3ff3 100644
--- a/dashboard/app/docs/page.tsx
+++ b/dashboard/app/docs/page.tsx
@@ -13,6 +13,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs' },
title: 'Overview',
description:
'What AgentGate is, the HTTP 402 + Casper mental model, the seller / buyer / operator roles, mock vs live modes, and a map of the full documentation.',
@@ -128,9 +129,10 @@ export default function Page() {
Mock vs live modes
- A single environment variable — AGENTGATE_MODE (default mock) — selects the
- chain backend behind the ChainClient seam. Everything above that seam is identical, so
- you build and test offline and flip one flag to go on-chain.
+ A single environment variable — AGENTGATE_MODE — selects the chain backend behind the{' '}
+ ChainClient seam. The stack (gateway, buyer agent, dashboard) defaults to mock via{' '}
+ loadConfig(), while the published npx CLI defaults to live. Everything above
+ the seam is identical, so you build and test offline and flip one flag to go on-chain.
-
Browse the docs — Get started
+
Get started
-
Guides
+
For sellers
+
+
For buyers
+
+
+
Run a gateway
+ Reference
diff --git a/dashboard/app/docs/protocol/page.tsx b/dashboard/app/docs/protocol/page.tsx
index 65c183e..29b1056 100644
--- a/dashboard/app/docs/protocol/page.tsx
+++ b/dashboard/app/docs/protocol/page.tsx
@@ -15,6 +15,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/protocol' },
title: 'How it works',
description:
'A concepts walkthrough of the x402 V1 payment protocol: discovery, the 402 PaymentRequiredResponse challenge (accepts[] / PaymentRequirements), a native CSPR transfer carrying the nonce as transfer_id, the X-PAYMENT proof header, on-chain verification, single-use nonce burning, the upstream proxy, and the attestation that feeds a trust score.',
@@ -45,7 +46,7 @@ export default function Page() {
body: (
<>
The agent reads the on-chain registry to find services and their{' '}
- trust scores, choosing one to call. Each
+ trust scores, choosing one to call. Each
service record carries a public gateway URL (/svc/:id) — never the upstream.
>
),
@@ -107,7 +108,7 @@ export default function Page() {
The gateway decodes the X-PAYMENT header, validates invoice state (nonce
must exist, be unused, and be within expiresAtMs), then calls{' '}
chain.verifyTransfer to check the transfer's target, amount, transfer
- id, and age (see Verification rules). A
+ id, and age (see Verification rules). A
still-settling transfer returns 402 with error:"settlement_pending"{' '}
and a Retry-After: 2 response header.
>
@@ -154,7 +155,7 @@ export default function Page() {
Each attestation increments the service's on-chain counters
(totalCalls, and successCalls on success). Those counters map to a
trust tier the next discovering agent reads (see{' '}
- Attestation and trust score).
+ Attestation and trust score).
>
),
},
diff --git a/dashboard/app/docs/quickstart/page.tsx b/dashboard/app/docs/quickstart/page.tsx
index 9d3a3a9..99a85f4 100644
--- a/dashboard/app/docs/quickstart/page.tsx
+++ b/dashboard/app/docs/quickstart/page.tsx
@@ -15,6 +15,7 @@ import {
} from '@/components/docs';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/quickstart' },
title: 'Quickstart',
description:
'From a clean clone to a verified register → 402 → pay → serve → attest → score loop in about a minute. Run the offline demo, view it live in the dashboard, then drive your first manual wrap by hand. Mock mode — no testnet keys or network required.',
@@ -65,9 +66,11 @@ export default function Page() {
/>
AGENTGATE_MODE selects the chain backend behind the ChainClient seam:{' '}
- mock (default — the in-process devnet used throughout this page, fully offline) or{' '}
- live (Casper Testnet, which needs PEM keys and a CSPR.cloud API key). Everything
- above the seam is identical. See Configuration.
+ mock (the in-process devnet used throughout this page, fully offline) or{' '}
+ live (Casper Testnet, which needs PEM keys and a CSPR.cloud API key). The stack
+ defaults to mock via loadConfig(), but the agentgate CLI defaults to{' '}
+ live — so the manual CLI commands below pass --mode mock. Everything above the
+ seam is identical. See Configuration.
60-second offline demo
@@ -187,13 +190,13 @@ export default function Page() {
/>
npm run dev seeds the devnet faucet and prints the exports for you. You can also
- mint a fresh pair any time with npm run agentgate -- demo-accounts. Both the buyer
+ mint a fresh pair any time with npm run agentgate -- demo-accounts --mode mock. Both the buyer
agent and agentgate wrap read these env vars to find their mock signer.
Wrap an API (seller side)
This registers the service on-chain (name, gateway URL, price in motes, payment target,
diff --git a/dashboard/app/docs/sdk/page.tsx b/dashboard/app/docs/sdk/page.tsx
index 6deaf77..6099f86 100644
--- a/dashboard/app/docs/sdk/page.tsx
+++ b/dashboard/app/docs/sdk/page.tsx
@@ -15,7 +15,12 @@ import {
StepFlow,
} from '@/components/docs';
-export const metadata = { title: 'Client SDK' };
+export const metadata = {
+ title: 'Client SDK',
+ description:
+ '@agentgate/client reference: createAgentGateClient and the fetchPaid helper that parses a 402, validates the x402 invoice, pays a native CSPR transfer, and retries with the X-PAYMENT proof — options, return shapes, and errors.',
+ alternates: { canonical: '/docs/sdk' },
+};
const INSTALL_IMPORT = [
"import { createAgentGateClient, parsePaymentRequired } from '@agentgate/client';",
diff --git a/dashboard/app/docs/security/page.tsx b/dashboard/app/docs/security/page.tsx
index 809a15e..a71b8f4 100644
--- a/dashboard/app/docs/security/page.tsx
+++ b/dashboard/app/docs/security/page.tsx
@@ -14,6 +14,7 @@ import {
import { CommandBlock } from '@/components/copy';
export const metadata: Metadata = {
+ alternates: { canonical: '/docs/security' },
title: 'Security model',
description:
'How AgentGate defends a money-moving gateway: SSRF guards against seller-controlled URLs, fail-closed live configuration, single-use payment nonces, secret redaction, rate limiting, request limits, and prompt-injection containment.',
diff --git a/dashboard/app/docs/sellers/page.tsx b/dashboard/app/docs/sellers/page.tsx
index 7993b10..6bf79df 100644
--- a/dashboard/app/docs/sellers/page.tsx
+++ b/dashboard/app/docs/sellers/page.tsx
@@ -14,7 +14,12 @@ import {
StepFlow,
} from '@/components/docs';
-export const metadata = { title: 'Wrap an API' };
+export const metadata = {
+ title: 'Wrap an API',
+ description:
+ 'Put an HTTP 402 paywall in front of your upstream API in one command: set a per-call price in CSPR, register the service on-chain, and collect native CSPR payments straight to your account.',
+ alternates: { canonical: '/docs/sellers' },
+};
export default function Page() {
return (
diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css
index b9636fc..01202d1 100644
--- a/dashboard/app/globals.css
+++ b/dashboard/app/globals.css
@@ -29,6 +29,15 @@ body {
color: #fff;
}
+@layer base {
+ /* Visible keyboard-focus indicator on every interactive element (WCAG 2.4.7 AA).
+ :focus-visible keeps it keyboard-only, so mouse clicks stay ring-free. */
+ :where(a, button, input, textarea, select, summary, [tabindex]:not([tabindex='-1'])):focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
@layer components {
/* Hairline panel with corner tick marks — the house style for data surfaces. */
.panel {
diff --git a/dashboard/app/layout.tsx b/dashboard/app/layout.tsx
index 355a261..0c456ad 100644
--- a/dashboard/app/layout.tsx
+++ b/dashboard/app/layout.tsx
@@ -3,31 +3,83 @@ import type { ReactNode } from 'react';
import { Inter, JetBrains_Mono, Space_Grotesk } from 'next/font/google';
import { Nav } from '@/components/nav';
import { Footer } from '@/components/footer';
+import { SITE_URL } from '@/lib/seo';
import './globals.css';
const display = Space_Grotesk({ subsets: ['latin'], variable: '--font-display' });
const body = Inter({ subsets: ['latin'], variable: '--font-body' });
const mono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' });
+const SITE_TAGLINE = 'AgentGate — Stripe for AI agents on Casper';
+const SITE_DESCRIPTION =
+ 'Wrap any API into a paid x402 service in one command — machine-to-machine CSPR micropayments with on-chain discovery and reputation on Casper.';
+
export const metadata: Metadata = {
+ metadataBase: new URL(SITE_URL),
+ applicationName: 'AgentGate',
title: {
- default: 'AgentGate — Stripe for AI agents on Casper',
+ default: SITE_TAGLINE,
template: '%s — AgentGate',
},
- description:
- 'Wrap any API into a paid x402 service in one command — machine-to-machine CSPR micropayments with on-chain discovery and reputation on Casper.',
+ description: SITE_DESCRIPTION,
+ alternates: { canonical: '/' },
+ openGraph: {
+ type: 'website',
+ siteName: 'AgentGate',
+ url: '/',
+ title: SITE_TAGLINE,
+ description: SITE_DESCRIPTION,
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: SITE_TAGLINE,
+ description: SITE_DESCRIPTION,
+ },
};
export const viewport: Viewport = {
themeColor: '#0A0E14',
};
+const jsonLd = {
+ '@context': 'https://schema.org',
+ '@graph': [
+ {
+ '@type': 'WebSite',
+ '@id': `${SITE_URL}/#website`,
+ url: SITE_URL,
+ name: 'AgentGate',
+ description: 'HTTP 402 payments for AI agents on Casper.',
+ },
+ {
+ '@type': 'SoftwareApplication',
+ name: 'AgentGate',
+ applicationCategory: 'DeveloperApplication',
+ operatingSystem: 'Any',
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
+ url: SITE_URL,
+ description:
+ 'Wrap any HTTP API into a paid x402 service with on-chain discovery and reputation on Casper.',
+ },
+ ],
+};
+
export default function RootLayout({ children }: { children: ReactNode }) {
return (
+
+
+ Skip to content
+
- {children}
+ {children}
diff --git a/dashboard/app/opengraph-image.tsx b/dashboard/app/opengraph-image.tsx
new file mode 100644
index 0000000..b48151b
--- /dev/null
+++ b/dashboard/app/opengraph-image.tsx
@@ -0,0 +1,51 @@
+import { ImageResponse } from 'next/og';
+
+export const alt = 'AgentGate — Stripe for AI agents on Casper';
+export const size = { width: 1200, height: 630 };
+export const contentType = 'image/png';
+
+// Site-wide social card. Pages without their own opengraph-image inherit this one.
+export default function Image() {
+ return new ImageResponse(
+ (
+
+
+ AGENTGATE
+
+
+
+ Stripe for AI agents on Casper
+
+
+ Wrap any API into a paid x402 service in one command — per-call CSPR micropayments with
+ on-chain discovery and reputation.
+
+
+
+ register → 402 → pay → serve → attest → score
+
+
+ ),
+ { ...size },
+ );
+}
diff --git a/dashboard/app/robots.ts b/dashboard/app/robots.ts
new file mode 100644
index 0000000..3a10490
--- /dev/null
+++ b/dashboard/app/robots.ts
@@ -0,0 +1,10 @@
+import type { MetadataRoute } from 'next';
+import { SITE_URL } from '@/lib/seo';
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: [{ userAgent: '*', allow: '/' }],
+ sitemap: `${SITE_URL}/sitemap.xml`,
+ host: SITE_URL,
+ };
+}
diff --git a/dashboard/app/sitemap.ts b/dashboard/app/sitemap.ts
new file mode 100644
index 0000000..168f307
--- /dev/null
+++ b/dashboard/app/sitemap.ts
@@ -0,0 +1,12 @@
+import type { MetadataRoute } from 'next';
+import { SITE_URL } from '@/lib/seo';
+import { DOC_LINKS } from '@/lib/doc-links';
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const routes = ['', '/catalog', '/activity', ...DOC_LINKS.map((l) => l.href)];
+ return routes.map((path) => ({
+ url: `${SITE_URL}${path}`,
+ changeFrequency: 'weekly',
+ priority: path === '' ? 1 : path === '/docs' ? 0.9 : path.startsWith('/docs') ? 0.7 : 0.6,
+ }));
+}
diff --git a/dashboard/components/docs-nav.tsx b/dashboard/components/docs-nav.tsx
index 1e8e9ef..f7f2973 100644
--- a/dashboard/components/docs-nav.tsx
+++ b/dashboard/components/docs-nav.tsx
@@ -3,74 +3,10 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
+import { DOC_GROUPS, DOC_LINKS } from '@/lib/doc-links';
-interface DocLink {
- href: string;
- label: string;
-}
-interface DocGroup {
- label: string;
- /** One-line descriptor shown under the group label to orient the reader. */
- hint?: string;
- links: DocLink[];
-}
-
-/**
- * Role-oriented IA: Get started → For sellers → For buyers → Run a gateway →
- * Concepts → Reference. Each role section gathers that role's guide + its primary
- * tool (sellers: CLI, buyers: SDK) so a reader sees their parts in one place.
- */
-export const DOC_GROUPS: DocGroup[] = [
- {
- label: 'Get started',
- links: [
- { href: '/docs', label: 'Overview' },
- { href: '/docs/quickstart', label: 'Quickstart' },
- { href: '/docs/installation', label: 'Installation' },
- ],
- },
- {
- label: 'For sellers',
- hint: 'Wrap your HTTP API into a paid, on-chain service.',
- links: [
- { href: '/docs/sellers', label: 'Wrap an API' },
- { href: '/docs/cli', label: 'CLI' },
- ],
- },
- {
- label: 'For buyers',
- hint: 'Build an agent that discovers, pays and rates services.',
- links: [
- { href: '/docs/buyers', label: 'Build an agent' },
- { href: '/docs/sdk', label: 'Client SDK' },
- ],
- },
- {
- label: 'Run a gateway',
- hint: 'Self-host the 402 middleware, or use the hosted one.',
- links: [{ href: '/docs/deployment', label: 'Deploy to production' }],
- },
- {
- label: 'Concepts',
- links: [
- { href: '/docs/protocol', label: 'How it works' },
- { href: '/docs/architecture', label: 'Architecture' },
- { href: '/docs/security', label: 'Security model' },
- ],
- },
- {
- label: 'Reference',
- links: [
- { href: '/docs/api', label: 'HTTP API' },
- { href: '/docs/configuration', label: 'Configuration' },
- { href: '/docs/contract', label: 'Smart contracts' },
- { href: '/docs/errors', label: 'Error codes' },
- ],
- },
-];
-
-/** Flat, ordered list (derived) — used for prev/next and any flat consumer. */
-export const DOC_LINKS = DOC_GROUPS.flatMap((g) => g.links);
+// Re-exported so existing importers of these from '@/components/docs-nav' keep working.
+export { DOC_GROUPS, DOC_LINKS };
function isActive(pathname: string, href: string): boolean {
if (href === '/docs') return pathname === '/docs';
diff --git a/dashboard/components/docs-search.tsx b/dashboard/components/docs-search.tsx
new file mode 100644
index 0000000..75a5956
--- /dev/null
+++ b/dashboard/components/docs-search.tsx
@@ -0,0 +1,135 @@
+'use client';
+
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { DOC_GROUPS } from '@/lib/doc-links';
+
+interface Hit {
+ href: string;
+ label: string;
+ group: string;
+}
+
+/**
+ * Command-palette search over the docs pages. Opens on ⌘K / Ctrl-K (or the
+ * trigger button), filters page titles + their section, and navigates on Enter.
+ * Purely client-side over the static IA — no index/build step.
+ */
+export function DocsSearch() {
+ const router = useRouter();
+ const [open, setOpen] = useState(false);
+ const [q, setQ] = useState('');
+ const [active, setActive] = useState(0);
+ const inputRef = useRef(null);
+
+ const index = useMemo(
+ () => DOC_GROUPS.flatMap((g) => g.links.map((l) => ({ href: l.href, label: l.label, group: g.label }))),
+ [],
+ );
+
+ const results = useMemo(() => {
+ const term = q.trim().toLowerCase();
+ if (!term) return index;
+ return index.filter(
+ (it) => it.label.toLowerCase().includes(term) || it.group.toLowerCase().includes(term),
+ );
+ }, [q, index]);
+
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
+ e.preventDefault();
+ setOpen((o) => !o);
+ } else if (e.key === 'Escape') {
+ setOpen(false);
+ }
+ };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, []);
+
+ useEffect(() => {
+ if (!open) return;
+ setQ('');
+ setActive(0);
+ const t = setTimeout(() => inputRef.current?.focus(), 0);
+ return () => clearTimeout(t);
+ }, [open]);
+
+ useEffect(() => setActive(0), [q]);
+
+ const go = (href: string) => {
+ setOpen(false);
+ router.push(href);
+ };
+
+ return (
+ <>
+
+
+ {open ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/dashboard/components/docs.tsx b/dashboard/components/docs.tsx
index b27167a..a1f36a9 100644
--- a/dashboard/components/docs.tsx
+++ b/dashboard/components/docs.tsx
@@ -1,8 +1,11 @@
import Link from 'next/link';
import type { ReactNode } from 'react';
+import { CopyButton } from './copy';
+import { DOCS_VERSION, REGISTRY_HASH_SHORT } from '@/lib/version';
-/* Server-side building blocks for the /docs section. No client code here —
- interactive pieces (copy buttons, active nav) live in copy.tsx / docs-nav.tsx. */
+/* Server-side building blocks for the /docs section. The only interactive piece
+ is the client CopyButton (from copy.tsx) embedded in CodeBlock and StepFlow;
+ active-nav and scroll-spy live in docs-nav.tsx. */
/** Page header — same microlabel + h1 + lede pattern as app/catalog/page.tsx. */
export function DocHeader({
@@ -21,6 +24,12 @@ export function DocHeader({
{title}