Dashboard and Stellar wallet integration for the AgentPay protocol (machine-to-machine payments on Stellar).
- Stack: Next.js 16, React, TypeScript, Tailwind CSS
- Purpose: AgentPay branding, dashboard placeholder, and future wallet/API integration
- Node.js 18+
- npm
-
Clone the repo (or add remote and pull):
git clone <repo-url> && cd agentpay-frontend
-
Install dependencies:
npm install
-
Verify setup:
npm run build npm test -
Run locally:
npm run dev
Open http://localhost:3000.
agentpay-frontend/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── error.tsx
│ │ ├── not-found.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── page.tsx # /
│ │ ├── about/page.tsx # /about
│ │ ├── admin/page.tsx # /admin
│ │ ├── agents/page.tsx # /agents
│ │ │ └── [agent]/page.tsx # /agents/:agent
│ │ ├── api-keys/page.tsx # /api-keys
│ │ ├── changelog/page.tsx # /changelog
│ │ ├── docs/page.tsx # /docs
│ │ ├── events/page.tsx # /events
│ │ ├── export/page.tsx # /export
│ │ ├── search/page.tsx # /search
│ │ ├── services/page.tsx # /services
│ │ │ ├── [serviceId]/page.tsx # /services/:serviceId
│ │ │ ├── [serviceId]/agents/page.tsx # /services/:serviceId/agents
│ │ │ └── [serviceId]/edit/page.tsx # /services/:serviceId/edit
│ │ │ └── new/page.tsx # /services/new
│ │ ├── settings/page.tsx # /settings
│ │ ├── stats/page.tsx # /stats
│ │ ├── usage/page.tsx # /usage
│ │ ├── webhooks/page.tsx # /webhooks
│ │ └── (shared components & libs live outside app/)
│ ├── components/ # Reusable UI components
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ ├── Card.tsx
│ │ └── ...
│ └── lib/ # API client, hooks, formatting, etc.
│ ├── apiClient.ts
│ ├── resolveApiBase.ts
│ ├── useApi.ts
│ └── ...
├── package.json
├── jest.config.ts
├── jest.setup.ts
└── .github/workflows/
└── ci.yml # CI: build, test
Backend endpoints are taken from the companion documentation page src/app/docs/page.tsx and from the API client usage throughout src/app/*.
| Path | Purpose | Backend endpoints it calls |
|---|---|---|
/ |
Main dashboard landing | (check app code in src/app/page.tsx and any hooks it uses) |
/about |
About page | (static UI unless the page calls APIs) |
/admin |
Admin control surface (pause/unpause/status) | POST /api/v1/admin/pause, POST /api/v1/admin/unpause, (reads status via GET /api/v1/admin/status in code) |
/agents |
Agents overview | (reads agents list via /api/v1/agents in code) |
/agents/:agent |
Single-agent view | (reads agent details via /api/v1/agents/:agent in code) |
/api-keys |
API keys management | (list/create/delete/update endpoints in code) |
/changelog |
Changelog | (static or calls /api/v1/changelog depending on implementation) |
/docs |
Short API endpoint reference | GET /api/v1/openapi.json plus the prose list rendered from sections in src/app/docs/page.tsx (usage, settle, services, admin pause/unpause) |
/events |
Event log renderer | (reads events stream/poll via /api/v1/events endpoints in code) |
/export |
Export data | (calls export endpoints in code) |
/search |
Global search | (calls search endpoint in code) |
/services |
Services list | GET /api/v1/services (and/or list related endpoints in code) |
/services/:serviceId |
Service details | GET /api/v1/services/:serviceId (plus nested reads in code) |
/services/:serviceId/agents |
Agents for a given service | GET /api/v1/services/:serviceId/agents |
/services/:serviceId/edit |
Edit service | (reads service + submits via service update endpoints in code) |
/services/new |
Create service | POST /api/v1/services |
/settings |
User/app settings | (calls settings endpoints in code) |
/stats |
Statistics | (calls stats endpoints in code) |
/usage |
Usage totals & settlement workflow | POST /api/v1/usage, GET /api/v1/usage/:agent/:serviceId, POST /api/v1/settle |
/webhooks |
Webhooks management | (calls webhooks endpoints in code) |
See docs/components.md for the shared component catalog,
including prop tables, usage examples, and accessibility notes for the
primitives in src/components.
See docs/hooks.md for the shared hook reference, including
signatures, return shapes, cancellation and SSR notes, and usage examples for
the hooks in src/lib.
The App Router fallback in src/app/loading.tsx renders an
animated animate-pulse skeleton during route transitions. So that
assistive-technology users are not left on an apparently empty page, the skeleton is
wrapped in a role="status" / aria-live="polite" region carrying an sr-only
"Loading…" label — mirroring the pattern used by
src/components/Spinner.tsx. The individual skeleton
blocks are marked aria-hidden="true" so they are not announced one by one, and the
pulse animation is disabled for users who request reduced motion via the
prefers-reduced-motion rules in src/app/globals.css. This
satisfies WCAG 4.1.3 Status Messages.
Behaviour is covered by src/app/loading.test.tsx.
See docs/api-integration.md for the complete reference of
every backend endpoint the dashboard calls — request bodies, response shapes, the
shared ApiError envelope, the 204/no-body convention, and pause-flag semantics.
| Variable | Visibility | Default | Purpose |
|---|---|---|---|
NEXT_PUBLIC_AGENTPAY_API_BASE |
public (bundled into client JS) | http://localhost:3001 |
Base URL for the AgentPay backend. Validated by resolveApiBase() in src/lib/resolveApiBase.ts and rejected in production if non-https except for localhost / 127.0.0.1. |
NEXT_PUBLIC_AGENTPAY_SITE_ORIGIN |
public (metadata route output) | http://localhost:3000 |
Canonical frontend origin used by src/app/sitemap.ts and src/app/robots.ts. Set this per deployment, for example https://dashboard.example.com; trailing slashes are trimmed. |
Because the variable is NEXT_PUBLIC_*, its value is exposed to the browser. Never put API secrets in it - it is used only for routing public HTTP requests.
The dashboard exposes Next.js metadata routes for crawler discovery and crawl control:
src/app/sitemap.tsemits canonical URLs for the public static routes:/,/about,/docs, and/changelog.src/app/robots.tsallows public crawling from/and explicitly disallows operator-only dashboard surfaces:/admin,/api-keys,/webhooks, and/settings.- Both metadata routes derive absolute URLs from
NEXT_PUBLIC_AGENTPAY_SITE_ORIGIN, defaulting tohttp://localhost:3000for local development rather than hard-coding a production domain.
The following frontend routes are defined under src/app/:
| Route | Page |
|---|---|
/ |
Home |
/about |
About |
/admin |
Admin |
/agents |
Agents |
/agents/:agent |
Agent detail |
/api-keys |
API keys |
/changelog |
Changelog |
/docs |
Docs |
/events |
Events |
/export |
Export |
/search |
Search |
/services |
Services |
/services/:serviceId |
Service detail |
/services/:serviceId/agents |
Service agents |
/services/:serviceId/edit |
Edit service |
/services/new |
New service |
/settings |
Settings |
/stats |
Stats |
/usage |
Usage |
/webhooks |
Webhooks |
The home page (src/app/page.tsx) renders the primary navigation entry points (Manage services, View stats, Record usage, Agents, Docs) and the external Stellar link inside a <nav aria-label="Quick links"> landmark with a semantic <ul> / <li> list structure. This improves discoverability for screen-reader users.
A baseline security header set (CSP, X-Frame-Options: DENY, Referrer-Policy, X-Content-Type-Options, Permissions-Policy, HSTS) is wired up in next.config.ts via src/lib/securityHeaders.ts. The CSP connect-src directive tracks NEXT_PUBLIC_AGENTPAY_API_BASE automatically; <a href> links to external sites (https://stellar.org, etc.) remain navigable.
When rendering links:
- Any external link rendered with
target="_blank"must includerel="noopener noreferrer". - Any
hrefderived from backend/user data must be validated withsafeHref()fromsrc/lib/url.ts. Unsafe schemes likejavascript:anddata:are rejected.
| Path | Notes |
|---|---|
/ |
Home |
/about |
About |
/admin |
Admin |
/agents |
Agents |
/agents/:agent |
Agent detail |
/api-keys |
API keys |
/changelog |
Changelog |
/docs |
Docs |
/events |
Event log |
/export |
Export |
/search |
Search |
/services |
Services |
/services/:serviceId |
Service detail |
/services/:serviceId/agents |
Service agents |
/services/:serviceId/edit |
Edit service |
/services/new |
New service |
/settings |
Settings |
/stats |
Stats |
/usage |
Usage |
/webhooks |
Webhooks |
The /events page renders server-supplied JSON payloads. Each payload is serialised through safeStringify (src/lib/format.ts) with a hard cap (EVENT_PAYLOAD_MAX_CHARS, default 5,000 chars) and a visible …(truncated) marker. Circular references, BigInt, functions, and malformed timestamps are replaced with safe sentinels so a bad payload can't crash the page.
The /changelog page keeps using useApi("/api/v1/changelog") for loading release notes. When the backend returns { entries: [] }, it renders the shared EmptyState component with a clear "No changelog entries yet" message instead of an empty list. This branch is constant-time and adds no extra network calls.
The frontend formats currency (Stroops / XLM) consistently using the helper formatStroops (located in src/lib/format.ts):
- Stroops definition: 1 XLM = 10,000,000 stroops (Stellar's base unit).
- Sub-cent amounts: If the value converts to less than
0.01 XLM(but is non-zero), the formatting shows the amount in grouped rawstroops(e.g.,50,000 stroops). - Standard amounts: Standard amounts are formatted in grouped
XLMwith at least two and up to seven fraction digits so large values stay readable and fractional XLM is not hidden (e.g.,1.50 XLM,1,234.56789 XLM). - Zero amount: A zero price formats to
0 XLM.
User-facing copy is being centralized into a single typed catalog so a future localization effort has one place to translate. This is groundwork only — no i18n library is wired up yet and no rendered copy changes.
-
Single source of truth:
src/lib/messages.tsexports a typed, namespacedmessagesobject (one namespace per surface), mirroring the established pattern insrc/app/pageTitles.ts. It isas const, so keys and literal values are fully typed and a typo on a key fails at compile time. -
Consuming it: import
messagesand read the string instead of hard-coding it inline:import { messages } from "@/lib/messages"; export function Footer() { return <footer>{messages.footer.text}</footer>; }
-
Migrated so far:
src/components/Footer.tsx, the home pagesrc/app/page.tsx, and the about pagesrc/app/about/page.tsx. Follow the same pattern when touching other surfaces — add the string to a namespace inmessages.ts, then reference it from the component. -
Future i18n: because the catalog is framework-agnostic, adopting
next-intl/next-i18nextlater can wrap the same namespaced shape (e.g. a per-locale catalog keyed offMessages) without changing existing call sites. -
Tests:
src/lib/__tests__/messages.test.tsasserts keys resolve, there are no duplicate key paths, unknown keys fail at the type level, and the migrated surfaces still render the exact original copy.
The root layout keeps the home route on the default AgentPay title and applies the template "%s — AgentPay" to route-specific titles.
| Route | Title |
|---|---|
/ |
AgentPay |
/about |
About AgentPay |
/services |
Services |
/services/new |
New service |
/usage |
Usage metering |
/agents |
Agents |
/admin |
Admin |
/stats |
Stats |
/events |
Event log |
/webhooks |
Webhooks |
/api-keys |
API keys |
/search |
Search |
/services/[serviceId] |
Service {serviceId} |
/services/[serviceId]/edit |
Edit service {serviceId} |
/services/[serviceId]/agents |
Top agents {serviceId} |
/agents/[agent] |
Agent {agent} |
The /about page now exposes direct links to the dashboard surfaces described in its copy: /services, /usage, /docs, /events, /webhooks, /api-keys, and /admin.
The /services page now uses server-driven pagination with the shared Spinner, EmptyState, and Pagination components.
See also: docs/theming.md for the full theme system, anti-FOUC contract, and token architecture.
To prevent a flash of the wrong colour scheme (FOUC) when a user has chosen dark mode, a tiny blocking inline <script> is injected into <head> in src/app/layout.tsx before the body renders:
- Requests are sent as
GET /api/v1/services?page=N&limit=25. - The page assumes the backend returns a paged payload with
servicesoritems, pluspageandpageCount. - If the backend clamps an out-of-range request, the UI follows the server-provided
pageandpageCountso the visible indicator stays in sync. - Service rows link through to
/services/:serviceIdusing encoded IDs.
The /agents page lists every agent identity seen by the backend, paginated with the same Spinner, EmptyState, and Pagination primitives used by the services page.
- A summary line (
X unique agent(s) seen across Y service(s)) is loaded once fromGET /api/v1/statsand shown above the directory. If that request fails the list still renders normally. - Directory rows are fetched from
GET /api/v1/agents?page=N&limit=25. The backend may return the array under eitheragentsoritems. - Each row is a
<Link>to/agents/:agentwith the identifier fullyencodeURIComponent-encoded, so agents with slashes or other special characters route correctly. Paginationhides itself automatically whenpageCount ≤ 1, so no pagination bar appears for a single-page result.- Backend errors are surfaced as a
role="alert"paragraph; the pagination bar is suppressed while an error is shown.
| Command | Description |
| ----------------------- | --------------------------- | ----------------------------------- |
| npm run build | Production build |
| npm test | Run Jest tests |
| npm run test:coverage | Run Jest with coverage | (not defined in this repo snapshot) |
| npm run dev | Development server |
| npm run lint | Run ESLint |
| npm run typecheck | Run the TypeScript compiler |
On push/PR to main, GitHub Actions runs:
npm cinpm run buildnpm test
See CONTRIBUTING.md for the full contributor workflow, branch naming convention, local checks, and UI accessibility expectations.
MIT