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}. + + } + /> + +

2026-07-01 — Self-service mapping & docs hardening

+ + +

2026-06-30 — CLI published to npm

+ + +

2026-06-29 — Live on Casper Testnet

+ + + + + ); +} 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

@@ -222,6 +266,11 @@ export default function Page() { ], ]} /> +

+ 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.

{/* ───────────────────────────── pause ───────────────────────────── */} @@ -361,7 +427,7 @@ export default function Page() { />

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 ( +