diff --git a/README.md b/README.md
index f119722..ce19e41 100644
--- a/README.md
+++ b/README.md
@@ -228,7 +228,9 @@ The scoring engine indexes x402 settlements on-chain using the EIP-3009 `Authori
| `/v1/score/basic?wallet=0x…` | GET | Score, tier, confidence. 10 free calls/day. |
| `/v1/score/erc8004?wallet=0x…` | GET | ERC-8004-compatible reputation document with score, identity, certification, and publication status. |
| `/v1/certification/readiness?wallet=0x…` | GET | Check if a wallet can apply for certification, see blockers, and get the next step before paying. |
-| `/v1/certification/directory` | GET | Public directory of active certifications with score context and evaluator/standards links. |
+| `/v1/certification/review` | GET / POST | Submit a certification review request or inspect the latest reviewer status for a wallet. |
+| `/v1/certification/directory?limit=&tier=&search=&sort=` | GET | Public directory of active certifications with score context, trust links, and search/sort filters. |
+| `/directory` | GET | Trusted Endpoint Directory page for browsing certified agents in the browser. |
| `/v1/agent/register` | POST | Register your wallet. +10 identity bonus. |
| `/v1/score/compute` | POST | Queue background score computation. Returns jobId immediately. |
| `/v1/score/job/:jobId` | GET | Poll async job status (pending → complete). |
diff --git a/index.html b/index.html
index ecf0e99..d8ab06e 100644
--- a/index.html
+++ b/index.html
@@ -335,7 +335,7 @@
DJD is evolving from a score API into trust infrastructure for the agent economy: free wallet scoring, public certification, standards-ready documents, evaluator previews, and x402 gating in one stack. Start with the score, then layer on directory distribution and settlement policy.
2
diff --git a/openapi.json b/openapi.json
index 7cc431a..951abb5 100644
--- a/openapi.json
+++ b/openapi.json
@@ -1980,6 +1980,72 @@
}
}
},
+ "/v1/certification/review": {
+ "get": {
+ "tags": ["certification"],
+ "summary": "Get certification review status",
+ "description": "Returns the latest certification review request and reviewer status for a wallet.",
+ "operationId": "getCertificationReview",
+ "parameters": [
+ {
+ "name": "wallet",
+ "in": "query",
+ "required": true,
+ "description": "Ethereum wallet address",
+ "schema": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Certification review returned",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/CertificationReviewResponse" }
+ }
+ }
+ },
+ "400": { "$ref": "#/components/responses/BadRequest" },
+ "404": { "$ref": "#/components/responses/NotFound" }
+ }
+ },
+ "post": {
+ "tags": ["certification"],
+ "summary": "Submit certification review request",
+ "description": "Creates a certification review request for an eligible wallet before the final certification purchase.",
+ "operationId": "submitCertificationReview",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/CertificationReviewRequest" }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Existing pending certification review returned",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/CertificationReviewResponse" }
+ }
+ }
+ },
+ "201": {
+ "description": "Certification review request created",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/CertificationReviewResponse" }
+ }
+ }
+ },
+ "400": { "$ref": "#/components/responses/BadRequest" },
+ "409": {
+ "description": "Wallet already has an active certification",
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
+ }
+ }
+ }
+ },
"/v1/certification/directory": {
"get": {
"tags": ["certification"],
@@ -2000,6 +2066,24 @@
"required": false,
"description": "Filter to a specific certification tier",
"schema": { "type": "string" }
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "required": false,
+ "description": "Filter by wallet, profile name, description, GitHub URL, or website URL",
+ "schema": { "type": "string" }
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "required": false,
+ "description": "Sort the directory by score, confidence, recency, or name",
+ "schema": {
+ "type": "string",
+ "enum": ["score", "confidence", "recent", "name"],
+ "default": "score"
+ }
}
],
"responses": {
@@ -3529,7 +3613,11 @@
"not_registered",
"score_missing",
"score_expired",
- "score_too_low"
+ "score_too_low",
+ "review_pending",
+ "review_approved",
+ "review_needs_info",
+ "review_rejected"
]
},
"requirements": {
@@ -3561,6 +3649,21 @@
"granted_at": { "type": "string", "format": "date-time", "nullable": true },
"expires_at": { "type": "string", "format": "date-time", "nullable": true }
}
+ },
+ "review": {
+ "type": "object",
+ "properties": {
+ "exists": { "type": "boolean" },
+ "status": {
+ "type": "string",
+ "nullable": true,
+ "enum": ["pending", "approved", "needs_info", "rejected"]
+ },
+ "requested_at": { "type": "string", "format": "date-time", "nullable": true },
+ "reviewed_at": { "type": "string", "format": "date-time", "nullable": true },
+ "reviewed_by": { "type": "string", "nullable": true },
+ "review_note": { "type": "string", "nullable": true }
+ }
}
}
},
@@ -3605,11 +3708,71 @@
"certify_readiness": { "type": "string", "format": "uri" },
"certify_overview": { "type": "string", "format": "uri" },
"certified_directory": { "type": "string", "format": "uri" },
- "apply_endpoint": { "type": "string", "format": "uri" }
+ "apply_endpoint": { "type": "string", "format": "uri" },
+ "review_status": { "type": "string", "format": "uri" }
}
}
}
},
+ "CertificationReviewRequest": {
+ "type": "object",
+ "required": ["wallet"],
+ "properties": {
+ "wallet": { "type": "string", "pattern": "^0x[0-9a-fA-F]{40}$" },
+ "note": { "type": "string", "maxLength": 500 }
+ }
+ },
+ "CertificationReviewResponse": {
+ "type": "object",
+ "properties": {
+ "id": { "type": "integer" },
+ "wallet": { "type": "string" },
+ "requested_by_wallet": { "type": "string" },
+ "status": {
+ "type": "string",
+ "enum": ["pending", "approved", "needs_info", "rejected"]
+ },
+ "requested_at": { "type": "string", "format": "date-time" },
+ "updated_at": { "type": "string", "format": "date-time" },
+ "reviewed_at": { "type": "string", "format": "date-time", "nullable": true },
+ "reviewed_by": { "type": "string", "nullable": true },
+ "requested_score": { "type": "integer" },
+ "requested_tier": { "type": "string" },
+ "requested_confidence": { "type": "number", "nullable": true },
+ "score_expires_at": { "type": "string", "format": "date-time", "nullable": true },
+ "request_note": { "type": "string", "nullable": true },
+ "review_note": { "type": "string", "nullable": true },
+ "current_score": {
+ "type": "object",
+ "properties": {
+ "score": { "type": "integer", "nullable": true },
+ "tier": { "type": "string", "nullable": true },
+ "confidence": { "type": "number", "nullable": true }
+ }
+ },
+ "profile": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string", "nullable": true },
+ "description": { "type": "string", "nullable": true },
+ "github_url": { "type": "string", "format": "uri", "nullable": true },
+ "website_url": { "type": "string", "format": "uri", "nullable": true },
+ "github_verified": { "type": "boolean" }
+ }
+ },
+ "links": {
+ "type": "object",
+ "properties": {
+ "agent_profile": { "type": "string", "format": "uri" },
+ "readiness": { "type": "string", "format": "uri" },
+ "review_status": { "type": "string", "format": "uri" },
+ "apply_endpoint": { "type": "string", "format": "uri" },
+ "certified_directory": { "type": "string", "format": "uri" }
+ }
+ },
+ "message": { "type": "string" }
+ }
+ },
"CertificationDirectoryResponse": {
"type": "object",
"properties": {
@@ -3618,9 +3781,15 @@
"type": "object",
"properties": {
"limit": { "type": "integer" },
- "tier": { "type": "string", "nullable": true }
+ "tier": { "type": "string", "nullable": true },
+ "search": { "type": "string", "nullable": true },
+ "sort": {
+ "type": "string",
+ "enum": ["score", "confidence", "recent", "name"]
+ }
}
},
+ "total": { "type": "integer" },
"returned": { "type": "integer" },
"certifications": {
"type": "array",
diff --git a/src/app.ts b/src/app.ts
index c5390a8..2101027 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -33,6 +33,7 @@ import certificationRoute from './routes/certification.js'
import certifyRoute from './routes/certify.js'
import clusterRoute from './routes/cluster.js'
import dataRoute from './routes/data.js'
+import directoryRoute from './routes/directory.js'
import docsRoute from './routes/docs.js'
import economyRoute from './routes/economy.js'
import explorerRoute from './routes/explorer.js'
@@ -50,6 +51,7 @@ import pricingRoute from './routes/pricing.js'
import ratingsRoute from './routes/ratings.js'
import registerRoute from './routes/register.js'
import reportRoute from './routes/report.js'
+import reviewerRoute from './routes/reviewer.js'
import scoreRoute from './routes/score.js'
import stakeRoute from './routes/stake.js'
import stripeWebhookRoute from './routes/stripeWebhook.js'
@@ -117,6 +119,7 @@ app.route('/v1/analytics', analyticsRoute)
app.route('/v1/agent/register', registerRoute)
app.route('/v1/badge', badgeRoute)
app.route('/explorer', explorerRoute)
+app.route('/directory', directoryRoute)
app.route('/certify', certifyRoute)
app.route('/blog', blogRoute)
app.route('/agent', agentRoute)
@@ -126,6 +129,7 @@ app.route('/docs', docsRoute)
app.route('/metrics', metricsRoute)
app.route('/billing', billingRoute)
app.route('/portal', portalRoute)
+app.route('/reviewer', reviewerRoute)
app.route('/pricing', pricingRoute)
app.route('/methodology', methodologyRoute)
app.route('/.well-known/x402', wellKnownRoute)
diff --git a/src/db/certificationQueries.ts b/src/db/certificationQueries.ts
index 1fb4578..e2a7d25 100644
--- a/src/db/certificationQueries.ts
+++ b/src/db/certificationQueries.ts
@@ -19,10 +19,7 @@ const stmtListCertifications = db.prepare<[], CertificationRow>(`
SELECT * FROM certifications ORDER BY granted_at DESC
`)
-const stmtListActiveCertificationDirectory = db.prepare<
- [string | null, string | null, number],
- CertificationDirectoryRow
->(`
+const stmtListActiveCertificationDirectory = db.prepare<[string | null, string | null], CertificationDirectoryRow>(`
SELECT
c.id,
c.wallet,
@@ -49,7 +46,6 @@ const stmtListActiveCertificationDirectory = db.prepare<
AND c.expires_at > datetime('now')
AND (? IS NULL OR c.tier = ?)
ORDER BY COALESCE(s.composite_score, c.score_at_certification) DESC, c.granted_at DESC
- LIMIT ?
`)
const stmtRevokeCertification = db.prepare(`
@@ -58,6 +54,90 @@ const stmtRevokeCertification = db.prepare(`
WHERE id = ? AND is_active = 1
`)
+const CERTIFICATION_REVIEW_SELECT = `
+ SELECT
+ r.id,
+ r.wallet,
+ r.requested_by_wallet,
+ r.requested_tier,
+ r.requested_score,
+ r.requested_confidence,
+ r.score_expires_at,
+ r.request_note,
+ r.status,
+ r.requested_at,
+ r.updated_at,
+ r.reviewed_at,
+ r.reviewed_by,
+ r.review_note,
+ reg.name,
+ reg.description,
+ reg.github_url,
+ reg.website_url,
+ COALESCE(reg.github_verified, 0) AS github_verified,
+ s.composite_score AS current_score,
+ s.tier AS current_tier,
+ s.confidence AS current_confidence
+ FROM certification_review_requests r
+ LEFT JOIN agent_registrations reg ON reg.wallet = r.wallet
+ LEFT JOIN scores s ON LOWER(s.wallet) = r.wallet
+`
+
+const stmtInsertCertificationReviewRequest = db.prepare(`
+ INSERT INTO certification_review_requests (
+ wallet,
+ requested_by_wallet,
+ requested_tier,
+ requested_score,
+ requested_confidence,
+ score_expires_at,
+ request_note
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
+`)
+
+const stmtGetCertificationReviewRequestById = db.prepare<[number], CertificationReviewRequestRow>(`
+ ${CERTIFICATION_REVIEW_SELECT}
+ WHERE r.id = ?
+ LIMIT 1
+`)
+
+const stmtGetLatestCertificationReviewRequest = db.prepare<[string], CertificationReviewRequestRow>(`
+ ${CERTIFICATION_REVIEW_SELECT}
+ WHERE r.wallet = ?
+ ORDER BY r.requested_at DESC, r.id DESC
+ LIMIT 1
+`)
+
+const stmtGetPendingCertificationReviewRequest = db.prepare<[string], CertificationReviewRequestRow>(`
+ ${CERTIFICATION_REVIEW_SELECT}
+ WHERE r.wallet = ?
+ AND r.status = 'pending'
+ ORDER BY r.requested_at DESC, r.id DESC
+ LIMIT 1
+`)
+
+const stmtListCertificationReviewRequests = db.prepare<
+ [string | null, string | null, number],
+ CertificationReviewRequestRow
+>(
+ `
+ ${CERTIFICATION_REVIEW_SELECT}
+ WHERE (? IS NULL OR r.status = ?)
+ ORDER BY r.requested_at DESC, r.id DESC
+ LIMIT ?
+ `,
+)
+
+const stmtUpdateCertificationReviewRequestDecision = db.prepare(`
+ UPDATE certification_review_requests
+ SET status = ?,
+ updated_at = datetime('now'),
+ reviewed_at = datetime('now'),
+ reviewed_by = ?,
+ review_note = ?
+ WHERE id = ?
+`)
+
const stmtCountCertifications = db.prepare<[], { count: number }>(`
SELECT COUNT(*) as count FROM certifications
`)
@@ -125,6 +205,31 @@ export interface CertificationDirectoryRow extends CertificationRow {
github_verified: number
}
+export interface CertificationReviewRequestRow {
+ id: number
+ wallet: string
+ requested_by_wallet: string
+ requested_tier: string
+ requested_score: number
+ requested_confidence: number | null
+ score_expires_at: string | null
+ request_note: string | null
+ status: string
+ requested_at: string
+ updated_at: string
+ reviewed_at: string | null
+ reviewed_by: string | null
+ review_note: string | null
+ name: string | null
+ description: string | null
+ github_url: string | null
+ website_url: string | null
+ github_verified: number
+ current_score: number | null
+ current_tier: string | null
+ current_confidence: number | null
+}
+
export function getActiveCertification(wallet: string): CertificationRow | undefined {
return stmtGetActiveCertification.get(wallet)
}
@@ -138,8 +243,8 @@ export function listCertifications(): CertificationRow[] {
return stmtListCertifications.all()
}
-export function listActiveCertificationDirectory(limit: number, tier?: string | null): CertificationDirectoryRow[] {
- return stmtListActiveCertificationDirectory.all(tier ?? null, tier ?? null, limit)
+export function listActiveCertificationDirectory(tier?: string | null): CertificationDirectoryRow[] {
+ return stmtListActiveCertificationDirectory.all(tier ?? null, tier ?? null)
}
export function revokeCertification(id: number, reason: string): boolean {
@@ -162,3 +267,49 @@ export function getCertificationRevenueSummary(): CertificationRevenueSummary {
by_month: byMonth,
}
}
+
+export function insertCertificationReviewRequest(
+ wallet: string,
+ requestedByWallet: string,
+ requestedTier: string,
+ requestedScore: number,
+ requestedConfidence: number | null,
+ scoreExpiresAt: string | null,
+ requestNote: string | null,
+): CertificationReviewRequestRow {
+ const result = stmtInsertCertificationReviewRequest.run(
+ wallet,
+ requestedByWallet,
+ requestedTier,
+ requestedScore,
+ requestedConfidence,
+ scoreExpiresAt,
+ requestNote,
+ )
+ return stmtGetCertificationReviewRequestById.get(Number(result.lastInsertRowid))!
+}
+
+export function getCertificationReviewRequestById(id: number): CertificationReviewRequestRow | undefined {
+ return stmtGetCertificationReviewRequestById.get(id)
+}
+
+export function getLatestCertificationReviewRequest(wallet: string): CertificationReviewRequestRow | undefined {
+ return stmtGetLatestCertificationReviewRequest.get(wallet)
+}
+
+export function getPendingCertificationReviewRequest(wallet: string): CertificationReviewRequestRow | undefined {
+ return stmtGetPendingCertificationReviewRequest.get(wallet)
+}
+
+export function listCertificationReviewRequests(status: string | null, limit: number): CertificationReviewRequestRow[] {
+ return stmtListCertificationReviewRequests.all(status, status, limit)
+}
+
+export function updateCertificationReviewRequestDecision(
+ id: number,
+ status: string,
+ reviewedBy: string,
+ reviewNote: string | null,
+): boolean {
+ return stmtUpdateCertificationReviewRequestDecision.run(status, reviewedBy, reviewNote, id).changes > 0
+}
diff --git a/src/db/schema.ts b/src/db/schema.ts
index 8817125..14ba350 100644
--- a/src/db/schema.ts
+++ b/src/db/schema.ts
@@ -634,6 +634,43 @@ db.exec(`
WHERE is_active = 1;
`)
+db.exec(`
+ CREATE TABLE IF NOT EXISTS certification_review_requests (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ wallet TEXT NOT NULL,
+ requested_by_wallet TEXT NOT NULL,
+ requested_tier TEXT NOT NULL,
+ requested_score INTEGER NOT NULL,
+ requested_confidence REAL,
+ score_expires_at TEXT,
+ request_note TEXT,
+ status TEXT NOT NULL DEFAULT 'pending',
+ requested_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
+ reviewed_at TEXT,
+ reviewed_by TEXT,
+ review_note TEXT
+ );
+`)
+
+addColumnIfMissing('certification_review_requests', 'requested_by_wallet', 'TEXT NOT NULL DEFAULT ""')
+addColumnIfMissing('certification_review_requests', 'requested_tier', 'TEXT NOT NULL DEFAULT "Trusted"')
+addColumnIfMissing('certification_review_requests', 'requested_score', 'INTEGER NOT NULL DEFAULT 0')
+addColumnIfMissing('certification_review_requests', 'requested_confidence', 'REAL')
+addColumnIfMissing('certification_review_requests', 'score_expires_at', 'TEXT')
+addColumnIfMissing('certification_review_requests', 'request_note', 'TEXT')
+addColumnIfMissing('certification_review_requests', 'status', 'TEXT NOT NULL DEFAULT "pending"')
+addColumnIfMissing('certification_review_requests', 'requested_at', "TEXT NOT NULL DEFAULT (datetime('now'))")
+addColumnIfMissing('certification_review_requests', 'updated_at', "TEXT NOT NULL DEFAULT (datetime('now'))")
+addColumnIfMissing('certification_review_requests', 'reviewed_at', 'TEXT')
+addColumnIfMissing('certification_review_requests', 'reviewed_by', 'TEXT')
+addColumnIfMissing('certification_review_requests', 'review_note', 'TEXT')
+
+db.exec(`
+ CREATE INDEX IF NOT EXISTS idx_cert_review_wallet ON certification_review_requests(wallet, requested_at DESC);
+ CREATE INDEX IF NOT EXISTS idx_cert_review_status ON certification_review_requests(status, requested_at DESC);
+`)
+
// ── ERC-8004 Reputation Publications ────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS reputation_publications (
diff --git a/src/routes/certification.ts b/src/routes/certification.ts
index f4adc5f..2a2715b 100644
--- a/src/routes/certification.ts
+++ b/src/routes/certification.ts
@@ -3,6 +3,8 @@
*
* Public (free):
* GET /readiness — check whether a wallet can apply for certification
+ * GET /review — inspect the latest certification review request for a wallet
+ * POST /review — submit a certification review request
* GET /directory — browse active certifications
* GET /:wallet — check certification status
* GET /badge/:wallet — SVG badge (green if certified, gray if not)
@@ -24,9 +26,14 @@ import {
getCertificationRevenue,
getCertificationBadgeView,
getCertificationDirectoryView,
+ issueCertificationFromReviewRequest,
getCertificationReadinessView,
+ getCertificationReviewStatusView,
getCertificationStatusView,
+ listCertificationReviewRequestViews,
+ reviewCertificationRequestDecision,
listCertificationRecords,
+ submitCertificationReviewRequest,
revokeCertificationRecord,
} from '../services/certificationService.js'
import { getPayerWallet } from '../utils/paymentUtils.js'
@@ -50,6 +57,8 @@ certification.get('/directory', (c) => {
const outcome = getCertificationDirectoryView({
limit: c.req.query('limit'),
tier: c.req.query('tier'),
+ search: c.req.query('search'),
+ sort: c.req.query('sort'),
})
if (!outcome.ok) {
return c.json(errorResponse(outcome.code, outcome.message, outcome.details), outcome.status)
@@ -58,6 +67,28 @@ certification.get('/directory', (c) => {
return c.json(outcome.data)
})
+certification.get('/review', (c) => {
+ const outcome = getCertificationReviewStatusView(c.req.query('wallet'))
+ if (!outcome.ok) {
+ return c.json(errorResponse(outcome.code, outcome.message, outcome.details), outcome.status)
+ }
+
+ return c.json(outcome.data)
+})
+
+certification.post('/review', async (c) => {
+ const body = await c.req.json<{ wallet?: string; note?: string }>().catch(() => null)
+ const outcome = submitCertificationReviewRequest({
+ wallet: body?.wallet,
+ note: body?.note,
+ })
+ if (!outcome.ok) {
+ return c.json(errorResponse(outcome.code, outcome.message, outcome.details), outcome.status)
+ }
+
+ return c.json(outcome.data, outcome.status ?? 201)
+})
+
// ── Public: Check certification status ──────────────────────────────────────
certification.get('/:wallet', (c) => {
@@ -115,6 +146,48 @@ certification.get('/admin/all', adminAuth, (c) => {
return c.json({ certifications, count: certifications.length })
})
+certification.get('/admin/reviews', adminAuth, (c) => {
+ const outcome = listCertificationReviewRequestViews({
+ status: c.req.query('status'),
+ limit: c.req.query('limit'),
+ })
+ if (!outcome.ok) {
+ return c.json(errorResponse(outcome.code, outcome.message, outcome.details), outcome.status)
+ }
+
+ return c.json(outcome.data)
+})
+
+certification.post('/admin/reviews/:id/decision', adminAuth, async (c) => {
+ const id = Number(c.req.param('id'))
+ const body = await c.req
+ .json<{ decision?: string; note?: string; reviewed_by?: string }>()
+ .catch(() => null)
+
+ const outcome = reviewCertificationRequestDecision({
+ id,
+ decision: body?.decision,
+ note: body?.note,
+ reviewedBy: body?.reviewed_by,
+ })
+ if (!outcome.ok) {
+ return c.json(errorResponse(outcome.code, outcome.message, outcome.details), outcome.status)
+ }
+
+ return c.json(outcome.data)
+})
+
+certification.post('/admin/reviews/:id/issue', adminAuth, (c) => {
+ const outcome = issueCertificationFromReviewRequest({
+ id: Number(c.req.param('id')),
+ })
+ if (!outcome.ok) {
+ return c.json(errorResponse(outcome.code, outcome.message, outcome.details), outcome.status)
+ }
+
+ return c.json(outcome.data, outcome.status ?? 201)
+})
+
// ── Admin: Revoke a certification ───────────────────────────────────────────
certification.post('/admin/:id/revoke', adminAuth, async (c) => {
diff --git a/src/routes/directory.ts b/src/routes/directory.ts
new file mode 100644
index 0000000..4d9f6a1
--- /dev/null
+++ b/src/routes/directory.ts
@@ -0,0 +1,19 @@
+import { Hono } from 'hono'
+import { directoryPageHtml } from '../templates/directory.js'
+
+const directory = new Hono()
+
+directory.get('/', (c) => {
+ c.header('Content-Type', 'text/html; charset=utf-8')
+ c.header('Cache-Control', 'public, max-age=300')
+ return c.body(
+ directoryPageHtml({
+ limit: c.req.query('limit'),
+ tier: c.req.query('tier'),
+ search: c.req.query('search'),
+ sort: c.req.query('sort'),
+ }),
+ )
+})
+
+export default directory
diff --git a/src/routes/legal.ts b/src/routes/legal.ts
index fd0a04a..9cbc34d 100644
--- a/src/routes/legal.ts
+++ b/src/routes/legal.ts
@@ -36,6 +36,7 @@ legal.get('/robots.txt', (c) => {
'User-agent: *\n' +
'Allow: /\n' +
'Allow: /certify\n' +
+ 'Allow: /directory\n' +
'Allow: /docs\n' +
'Allow: /methodology\n' +
'Allow: /pricing\n' +
@@ -44,6 +45,7 @@ legal.get('/robots.txt', (c) => {
'Disallow: /metrics\n' +
'Disallow: /stripe\n' +
'Disallow: /portal\n' +
+ 'Disallow: /reviewer\n' +
'\n' +
`Sitemap: ${buildPublicUrl('/openapi.json')}\n`,
)
diff --git a/src/routes/reviewer.ts b/src/routes/reviewer.ts
new file mode 100644
index 0000000..5377c3b
--- /dev/null
+++ b/src/routes/reviewer.ts
@@ -0,0 +1,12 @@
+import { Hono } from 'hono'
+import { reviewerPageHtml } from '../templates/reviewer.js'
+
+const reviewer = new Hono()
+
+reviewer.get('/', (c) => {
+ c.header('Content-Type', 'text/html; charset=utf-8')
+ c.header('Cache-Control', 'no-store')
+ return c.body(reviewerPageHtml())
+})
+
+export default reviewer
diff --git a/src/services/certificationService.ts b/src/services/certificationService.ts
index 32c7997..88d039c 100644
--- a/src/services/certificationService.ts
+++ b/src/services/certificationService.ts
@@ -1,15 +1,26 @@
import { buildPublicUrl } from '../config/public.js'
-import type { CertificationDirectoryRow, CertificationRevenueSummary, CertificationRow } from '../db.js'
+import type {
+ CertificationDirectoryRow,
+ CertificationRevenueSummary,
+ CertificationReviewRequestRow,
+ CertificationRow,
+} from '../db.js'
import {
db,
getActiveCertification,
getCertificationRevenueSummary,
+ getCertificationReviewRequestById,
+ getLatestCertificationReviewRequest,
+ getPendingCertificationReviewRequest,
getRegistration,
getScore,
insertCertification,
+ insertCertificationReviewRequest,
listActiveCertificationDirectory,
+ listCertificationReviewRequests,
listCertifications,
revokeCertification,
+ updateCertificationReviewRequestDecision,
} from '../db.js'
import { makeBadge } from '../utils/badgeGenerator.js'
import { normalizeWallet } from '../utils/walletUtils.js'
@@ -24,6 +35,8 @@ export interface CertificationApplyError {
| 'cert_not_registered'
| 'cert_already_active'
| 'cert_not_found'
+ | 'cert_review_not_found'
+ | 'cert_review_not_approved'
message: string
status: 400 | 404 | 409
details?: Record
@@ -32,7 +45,7 @@ export interface CertificationApplyError {
interface CertificationSuccess {
ok: true
data: T
- status?: 201
+ status?: 200 | 201
}
export type CertificationResult = CertificationApplyError | CertificationSuccess
@@ -103,7 +116,10 @@ export interface CertificationDirectoryView {
filters: {
limit: number
tier: string | null
+ search: string | null
+ sort: CertificationDirectorySort
}
+ total: number
returned: number
certifications: CertificationDirectoryEntryView[]
}
@@ -111,7 +127,17 @@ export interface CertificationDirectoryView {
export interface CertificationReadinessView {
wallet: string
can_apply: boolean
- status: 'eligible' | 'already_certified' | 'not_registered' | 'score_missing' | 'score_expired' | 'score_too_low'
+ status:
+ | 'eligible'
+ | 'already_certified'
+ | 'not_registered'
+ | 'score_missing'
+ | 'score_expired'
+ | 'score_too_low'
+ | 'review_pending'
+ | 'review_approved'
+ | 'review_needs_info'
+ | 'review_rejected'
requirements: {
registration: {
met: boolean
@@ -131,6 +157,14 @@ export interface CertificationReadinessView {
granted_at: string | null
expires_at: string | null
}
+ review: {
+ exists: boolean
+ status: CertificationReviewStatus | null
+ requested_at: string | null
+ reviewed_at: string | null
+ reviewed_by: string | null
+ review_note: string | null
+ }
}
blockers: Array<{
code: string
@@ -151,7 +185,65 @@ export interface CertificationReadinessView {
certify_overview: string
certified_directory: string
apply_endpoint: string
+ review_status: string
+ }
+}
+
+export const CERTIFICATION_REVIEW_STATUSES = ['pending', 'approved', 'needs_info', 'rejected'] as const
+export type CertificationReviewStatus = (typeof CERTIFICATION_REVIEW_STATUSES)[number]
+
+interface CertificationReviewLinks {
+ agent_profile: string
+ readiness: string
+ review_status: string
+ apply_endpoint: string
+ certified_directory: string
+}
+
+export interface CertificationReviewRequestView {
+ id: number
+ wallet: string
+ requested_by_wallet: string
+ status: CertificationReviewStatus
+ requested_at: string
+ updated_at: string
+ reviewed_at: string | null
+ reviewed_by: string | null
+ requested_score: number
+ requested_tier: string
+ requested_confidence: number | null
+ score_expires_at: string | null
+ request_note: string | null
+ review_note: string | null
+ current_score: {
+ score: number | null
+ tier: string | null
+ confidence: number | null
}
+ profile: {
+ name: string | null
+ description: string | null
+ github_url: string | null
+ website_url: string | null
+ github_verified: boolean
+ }
+ links: CertificationReviewLinks
+ message: string
+}
+
+export interface CertificationReviewQueueView {
+ filters: {
+ status: CertificationReviewStatus | null
+ limit: number
+ }
+ returned: number
+ requests: CertificationReviewRequestView[]
+}
+
+export interface CertificationIssuedFromReviewView {
+ review: CertificationReviewRequestView
+ certification: CertificationApplyView
+ message: string
}
const applyForCertificationTxn = db.transaction((wallet: string): CertificationApplyResult => {
@@ -202,6 +294,9 @@ const applyForCertificationTxn = db.transaction((wallet: string): CertificationA
}
})
+export const CERTIFICATION_DIRECTORY_SORTS = ['score', 'confidence', 'recent', 'name'] as const
+export type CertificationDirectorySort = (typeof CERTIFICATION_DIRECTORY_SORTS)[number]
+
function invalidWalletError(message: string): CertificationApplyError {
return {
ok: false,
@@ -211,6 +306,29 @@ function invalidWalletError(message: string): CertificationApplyError {
}
}
+function normalizeReviewStatus(rawStatus: string | null | undefined): CertificationReviewStatus | null {
+ const normalized = rawStatus?.trim().toLowerCase()
+ if (!normalized) return null
+ if (CERTIFICATION_REVIEW_STATUSES.includes(normalized as CertificationReviewStatus)) {
+ return normalized as CertificationReviewStatus
+ }
+ return null
+}
+
+function normalizeReviewDecision(rawStatus: string | null | undefined): CertificationReviewStatus | null {
+ const normalized = normalizeReviewStatus(rawStatus)
+ if (normalized && normalized !== 'pending') {
+ return normalized
+ }
+ return null
+}
+
+function normalizeReviewNote(rawNote: string | null | undefined): string | null {
+ const note = rawNote?.trim()
+ if (!note) return null
+ return note.slice(0, 500)
+}
+
function buildCertificationLinks(wallet: string): CertificationStatusView['links'] {
return {
certification_badge: buildPublicUrl(`/v1/certification/badge/${wallet}`),
@@ -227,8 +345,19 @@ function buildCertificationReadinessLinks(wallet: string): CertificationReadines
...buildCertificationLinks(wallet),
certification_status: buildPublicUrl(`/v1/certification/${wallet}`),
certify_overview: buildPublicUrl(`/certify?wallet=${wallet}`),
- certified_directory: buildPublicUrl('/v1/certification/directory'),
+ certified_directory: buildPublicUrl('/directory'),
+ apply_endpoint: buildPublicUrl('/v1/certification/apply'),
+ review_status: buildPublicUrl(`/v1/certification/review?wallet=${wallet}`),
+ }
+}
+
+function buildCertificationReviewLinks(wallet: string): CertificationReviewLinks {
+ return {
+ agent_profile: buildPublicUrl(`/agent/${wallet}`),
+ readiness: buildPublicUrl(`/v1/certification/readiness?wallet=${wallet}`),
+ review_status: buildPublicUrl(`/v1/certification/review?wallet=${wallet}`),
apply_endpoint: buildPublicUrl('/v1/certification/apply'),
+ certified_directory: buildPublicUrl('/directory'),
}
}
@@ -293,6 +422,113 @@ function buildCertificationDirectoryEntryView(row: CertificationDirectoryRow): C
}
}
+function buildCertificationReviewMessage(status: CertificationReviewStatus): string {
+ if (status === 'approved') {
+ return 'Review request approved. This wallet is cleared for the next certification step.'
+ }
+ if (status === 'needs_info') {
+ return 'Review request needs more information before approval.'
+ }
+ if (status === 'rejected') {
+ return 'Review request rejected. Resolve the reviewer feedback before submitting again.'
+ }
+ return 'Review request submitted and waiting for reviewer action.'
+}
+
+function buildCertificationReviewView(row: CertificationReviewRequestRow): CertificationReviewRequestView {
+ return {
+ id: row.id,
+ wallet: row.wallet,
+ requested_by_wallet: row.requested_by_wallet,
+ status: row.status as CertificationReviewStatus,
+ requested_at: row.requested_at,
+ updated_at: row.updated_at,
+ reviewed_at: row.reviewed_at,
+ reviewed_by: row.reviewed_by,
+ requested_score: row.requested_score,
+ requested_tier: row.requested_tier,
+ requested_confidence: row.requested_confidence,
+ score_expires_at: row.score_expires_at,
+ request_note: row.request_note,
+ review_note: row.review_note,
+ current_score: {
+ score: row.current_score,
+ tier: row.current_tier,
+ confidence: row.current_confidence,
+ },
+ profile: {
+ name: row.name,
+ description: row.description,
+ github_url: row.github_url,
+ website_url: row.website_url,
+ github_verified: row.github_verified === 1,
+ },
+ links: buildCertificationReviewLinks(row.wallet),
+ message: buildCertificationReviewMessage(row.status as CertificationReviewStatus),
+ }
+}
+
+function normalizeDirectorySort(rawSort: string | null | undefined): CertificationDirectorySort {
+ const normalized = rawSort?.trim().toLowerCase()
+ if (normalized && CERTIFICATION_DIRECTORY_SORTS.includes(normalized as CertificationDirectorySort)) {
+ return normalized as CertificationDirectorySort
+ }
+ return 'score'
+}
+
+function matchesDirectorySearch(entry: CertificationDirectoryEntryView, search: string | null): boolean {
+ if (!search) return true
+
+ const haystack = [
+ entry.wallet,
+ entry.profile.name,
+ entry.profile.description,
+ entry.profile.github_url,
+ entry.profile.website_url,
+ ]
+ .filter((value): value is string => typeof value === 'string' && value.length > 0)
+ .join(' ')
+ .toLowerCase()
+
+ return haystack.includes(search)
+}
+
+function sortDirectoryEntries(
+ entries: CertificationDirectoryEntryView[],
+ sort: CertificationDirectorySort,
+): CertificationDirectoryEntryView[] {
+ const scoreValue = (entry: CertificationDirectoryEntryView) =>
+ entry.current_score.score ?? entry.certification.score_at_certification
+ const confidenceValue = (entry: CertificationDirectoryEntryView) => entry.current_score.confidence ?? -1
+ const grantedAtValue = (entry: CertificationDirectoryEntryView) => Date.parse(entry.certification.granted_at)
+ const nameValue = (entry: CertificationDirectoryEntryView) =>
+ (entry.profile.name?.trim().toLowerCase() || entry.wallet).toLowerCase()
+
+ return [...entries].sort((left, right) => {
+ if (sort === 'confidence') {
+ return (
+ confidenceValue(right) - confidenceValue(left) ||
+ scoreValue(right) - scoreValue(left) ||
+ grantedAtValue(right) - grantedAtValue(left)
+ )
+ }
+
+ if (sort === 'recent') {
+ return grantedAtValue(right) - grantedAtValue(left) || scoreValue(right) - scoreValue(left)
+ }
+
+ if (sort === 'name') {
+ return nameValue(left).localeCompare(nameValue(right)) || scoreValue(right) - scoreValue(left)
+ }
+
+ return (
+ scoreValue(right) - scoreValue(left) ||
+ confidenceValue(right) - confidenceValue(left) ||
+ grantedAtValue(right) - grantedAtValue(left)
+ )
+ })
+}
+
export function getCertificationStatus(wallet: string): CertificationRow | null {
return getActiveCertification(wallet) ?? null
}
@@ -308,6 +544,7 @@ export function getCertificationReadinessView(
const registration = getRegistration(wallet)
const score = getScore(wallet)
const certification = getActiveCertification(wallet)
+ const review = getLatestCertificationReviewRequest(wallet)
const nowIso = new Date().toISOString()
const links = buildCertificationReadinessLinks(wallet)
const scoreIsFresh = !!score && score.expires_at > nowIso
@@ -327,7 +564,7 @@ export function getCertificationReadinessView(
nextSteps = [
{ code: 'view_status', label: 'View certification status', href: links.certification_status },
{ code: 'open_profile', label: 'Open agent profile', href: links.agent_profile },
- { code: 'browse_directory', label: 'Browse certified directory', href: links.certified_directory },
+ { code: 'browse_directory', label: 'Browse certified directory', href: buildPublicUrl('/directory') },
]
} else if (!registration) {
status = 'not_registered'
@@ -371,12 +608,54 @@ export function getCertificationReadinessView(
nextSteps = [
{ code: 'review_profile', label: 'Review agent profile', href: links.agent_profile },
{ code: 'review_evaluator', label: 'Open evaluator preview', href: links.evaluator_preview },
- { code: 'browse_directory', label: 'See certified peers', href: links.certified_directory },
+ { code: 'browse_directory', label: 'See certified peers', href: buildPublicUrl('/directory') },
+ ]
+ } else if (review?.status === 'pending') {
+ status = 'review_pending'
+ blockers.push({
+ code: 'cert_review_pending',
+ message: 'A certification review request is already pending for this wallet.',
+ })
+ nextSteps = [
+ { code: 'review_status', label: 'Check review status', href: links.review_status },
+ { code: 'open_profile', label: 'Open agent profile', href: links.agent_profile },
+ { code: 'review_standards', label: 'Review ERC-8004 document', href: links.standards_document },
+ ]
+ } else if (review?.status === 'needs_info') {
+ status = 'review_needs_info'
+ blockers.push({
+ code: 'cert_review_needs_info',
+ message: 'A reviewer asked for more information before certification can proceed.',
+ })
+ nextSteps = [
+ { code: 'review_status', label: 'Read reviewer status', href: links.review_status },
+ { code: 'open_profile', label: 'Open agent profile', href: links.agent_profile },
+ { code: 'submit_review', label: 'Return to Certify', href: links.certify_overview },
+ ]
+ } else if (review?.status === 'rejected') {
+ status = 'review_rejected'
+ blockers.push({
+ code: 'cert_review_rejected',
+ message: 'The latest certification review request was rejected. Resolve the reviewer feedback first.',
+ })
+ nextSteps = [
+ { code: 'review_status', label: 'Read reviewer status', href: links.review_status },
+ { code: 'review_evaluator', label: 'Open evaluator preview', href: links.evaluator_preview },
+ { code: 'return_certify', label: 'Return to Certify', href: links.certify_overview },
+ ]
+ } else if (review?.status === 'approved') {
+ canApply = true
+ status = 'review_approved'
+ nextSteps = [
+ { code: 'review_status', label: 'Review approved status', href: links.review_status },
+ { code: 'apply', label: 'Submit certification purchase', href: links.apply_endpoint },
+ { code: 'open_profile', label: 'Open agent profile', href: links.agent_profile },
]
} else {
canApply = true
status = 'eligible'
nextSteps = [
+ { code: 'submit_review', label: 'Request a review packet', href: links.certify_overview },
{ code: 'apply', label: 'Submit certification purchase', href: links.apply_endpoint },
{ code: 'review_standards', label: 'Review ERC-8004 document', href: links.standards_document },
{ code: 'open_profile', label: 'Open agent profile', href: links.agent_profile },
@@ -408,6 +687,14 @@ export function getCertificationReadinessView(
granted_at: certification?.granted_at ?? null,
expires_at: certification?.expires_at ?? null,
},
+ review: {
+ exists: !!review,
+ status: (review?.status as CertificationReviewStatus | undefined) ?? null,
+ requested_at: review?.requested_at ?? null,
+ reviewed_at: review?.reviewed_at ?? null,
+ reviewed_by: review?.reviewed_by ?? null,
+ review_note: review?.review_note ?? null,
+ },
},
blockers,
next_steps: nextSteps,
@@ -421,15 +708,266 @@ export function getCertificationReadinessView(
}
}
+export function getCertificationReviewStatusView(
+ rawWallet: string | null | undefined,
+): CertificationResult {
+ const wallet = normalizeWallet(rawWallet)
+ if (!wallet) {
+ return invalidWalletError('Valid Ethereum wallet address required')
+ }
+
+ const reviewRequest = getLatestCertificationReviewRequest(wallet)
+ if (!reviewRequest) {
+ return {
+ ok: false,
+ code: 'cert_review_not_found',
+ message: 'No certification review request found for this wallet',
+ status: 404,
+ }
+ }
+
+ return {
+ ok: true,
+ data: buildCertificationReviewView(reviewRequest),
+ }
+}
+
+export function submitCertificationReviewRequest(params: {
+ wallet: string | null | undefined
+ note?: string | null | undefined
+}): CertificationResult {
+ const wallet = normalizeWallet(params.wallet)
+ if (!wallet) {
+ return invalidWalletError('Valid Ethereum wallet address required')
+ }
+
+ const latestReview = getLatestCertificationReviewRequest(wallet)
+ if (latestReview?.status === 'approved') {
+ return {
+ ok: true,
+ status: 200,
+ data: {
+ ...buildCertificationReviewView(latestReview),
+ message: 'Review request already approved for this wallet.',
+ },
+ }
+ }
+
+ const existingPending = getPendingCertificationReviewRequest(wallet)
+ if (existingPending) {
+ return {
+ ok: true,
+ status: 200,
+ data: {
+ ...buildCertificationReviewView(existingPending),
+ message: 'Review request already pending for this wallet.',
+ },
+ }
+ }
+
+ const score = getScore(wallet)
+ if (!score || score.expires_at <= new Date().toISOString()) {
+ return {
+ ok: false,
+ code: 'cert_requirements_not_met',
+ message: 'Score has expired — request a fresh score before submitting for review',
+ status: 400,
+ }
+ }
+
+ if (score.composite_score < 75) {
+ return {
+ ok: false,
+ code: 'cert_score_too_low',
+ message: 'Composite score must be >= 75 before a review request can be submitted',
+ status: 400,
+ details: { current_score: score.composite_score },
+ }
+ }
+
+ if (!getRegistration(wallet)) {
+ return {
+ ok: false,
+ code: 'cert_not_registered',
+ message: 'Agent must be registered before submitting a review request',
+ status: 400,
+ }
+ }
+
+ if (getActiveCertification(wallet)) {
+ return {
+ ok: false,
+ code: 'cert_already_active',
+ message: 'Wallet already has an active certification',
+ status: 409,
+ }
+ }
+
+ const reviewRequest = insertCertificationReviewRequest(
+ wallet,
+ wallet,
+ score.tier,
+ score.composite_score,
+ score.confidence ?? null,
+ score.expires_at ?? null,
+ normalizeReviewNote(params.note),
+ )
+
+ return {
+ ok: true,
+ status: 201,
+ data: buildCertificationReviewView(reviewRequest),
+ }
+}
+
+export function listCertificationReviewRequestViews(params: {
+ status?: string | null | undefined
+ limit?: string | null | undefined
+}): CertificationResult {
+ const parsedLimit = Number.parseInt(params.limit ?? '50', 10)
+ const limit = Number.isNaN(parsedLimit) ? 50 : Math.min(Math.max(parsedLimit, 1), 200)
+ const status = normalizeReviewStatus(params.status)
+ const requests = listCertificationReviewRequests(status, limit).map(buildCertificationReviewView)
+
+ return {
+ ok: true,
+ data: {
+ filters: {
+ status,
+ limit,
+ },
+ returned: requests.length,
+ requests,
+ },
+ }
+}
+
+export function reviewCertificationRequestDecision(params: {
+ id: number
+ decision: string | null | undefined
+ note?: string | null | undefined
+ reviewedBy?: string | null | undefined
+}): CertificationResult {
+ if (!Number.isInteger(params.id) || params.id <= 0) {
+ return {
+ ok: false,
+ code: 'invalid_request',
+ message: 'Invalid certification review request ID',
+ status: 400,
+ }
+ }
+
+ const decision = normalizeReviewDecision(params.decision)
+ if (!decision) {
+ return {
+ ok: false,
+ code: 'invalid_request',
+ message: 'Decision must be approved, needs_info, or rejected',
+ status: 400,
+ }
+ }
+
+ if (
+ !updateCertificationReviewRequestDecision(
+ params.id,
+ decision,
+ params.reviewedBy?.trim() || 'admin',
+ normalizeReviewNote(params.note),
+ )
+ ) {
+ return {
+ ok: false,
+ code: 'cert_review_not_found',
+ message: 'Certification review request not found',
+ status: 404,
+ }
+ }
+
+ const updated = getCertificationReviewRequestById(params.id)
+ if (!updated) {
+ return {
+ ok: false,
+ code: 'cert_review_not_found',
+ message: 'Certification review request not found',
+ status: 404,
+ }
+ }
+
+ return {
+ ok: true,
+ data: buildCertificationReviewView(updated),
+ }
+}
+
+export function issueCertificationFromReviewRequest(params: {
+ id: number
+}): CertificationResult {
+ if (!Number.isInteger(params.id) || params.id <= 0) {
+ return {
+ ok: false,
+ code: 'invalid_request',
+ message: 'Invalid certification review request ID',
+ status: 400,
+ }
+ }
+
+ const review = getCertificationReviewRequestById(params.id)
+ if (!review) {
+ return {
+ ok: false,
+ code: 'cert_review_not_found',
+ message: 'Certification review request not found',
+ status: 404,
+ }
+ }
+
+ if (review.status !== 'approved') {
+ return {
+ ok: false,
+ code: 'cert_review_not_approved',
+ message: 'Certification review must be approved before issuance',
+ status: 400,
+ }
+ }
+
+ const issuance = applyForCertification(review.wallet)
+ if (!issuance.ok) {
+ return issuance
+ }
+
+ return {
+ ok: true,
+ status: 201,
+ data: {
+ review: buildCertificationReviewView(review),
+ certification: issuance.data,
+ message: 'Certification issued from approved review request',
+ },
+ }
+}
+
export function getCertificationDirectoryView(params: {
limit: string | null | undefined
tier: string | null | undefined
+ search?: string | null | undefined
+ sort?: string | null | undefined
}): CertificationResult {
const parsedLimit = Number.parseInt(params.limit ?? '25', 10)
const limit = Number.isNaN(parsedLimit) ? 25 : Math.min(Math.max(parsedLimit, 1), 100)
const rawTier = params.tier?.trim()
const tier = rawTier && rawTier.length > 0 ? rawTier : null
- const certifications = listActiveCertificationDirectory(limit, tier).map(buildCertificationDirectoryEntryView)
+ const rawSearch = params.search?.trim().toLowerCase()
+ const search = rawSearch && rawSearch.length > 0 ? rawSearch : null
+ const sort = normalizeDirectorySort(params.sort)
+ const matchingCertifications = sortDirectoryEntries(
+ listActiveCertificationDirectory(tier)
+ .map(buildCertificationDirectoryEntryView)
+ .filter((entry) => {
+ return matchesDirectorySearch(entry, search)
+ }),
+ sort,
+ )
+ const certifications = matchingCertifications.slice(0, limit)
return {
ok: true,
@@ -438,7 +976,10 @@ export function getCertificationDirectoryView(params: {
filters: {
limit,
tier,
+ search,
+ sort,
},
+ total: matchingCertifications.length,
returned: certifications.length,
certifications,
},
diff --git a/src/services/discoveryService.ts b/src/services/discoveryService.ts
index 80203b0..26fa1f8 100644
--- a/src/services/discoveryService.ts
+++ b/src/services/discoveryService.ts
@@ -196,16 +196,37 @@ export function getX402DiscoveryView(requestUrl: string, forwardedProto?: string
},
},
},
+ {
+ path: '/v1/certification/review',
+ method: 'GET/POST',
+ price: 0,
+ description:
+ 'Free certification review queue surface for submitting a review request or reading the latest reviewer status for a wallet.',
+ input: {
+ query: { wallet: { type: 'string', required: true, description: 'Ethereum wallet address' } },
+ body: { wallet: { type: 'string', required: true }, note: { type: 'string', required: false } },
+ },
+ output: {
+ example: {
+ wallet: '0x…',
+ status: 'pending',
+ requested_tier: 'Trusted',
+ requested_score: 82,
+ },
+ },
+ },
{
path: '/v1/certification/directory',
method: 'GET',
price: 0,
description:
- 'Public directory of active DJD certifications with current score context, profile metadata, and links to standards and evaluator views.',
+ 'Public directory of active DJD certifications with current score context, profile metadata, trust links, and optional search/sort filters.',
input: {
query: {
limit: { type: 'integer' },
tier: { type: 'string' },
+ search: { type: 'string' },
+ sort: { type: 'string', enum: ['score', 'confidence', 'recent', 'name'] },
},
},
},
diff --git a/src/templates/agentProfile.ts b/src/templates/agentProfile.ts
index 2ed3571..a8e8fb2 100644
--- a/src/templates/agentProfile.ts
+++ b/src/templates/agentProfile.ts
@@ -82,7 +82,7 @@ export function renderAgentPage(
const certificationStatusUrl = `${safeOrigin}/v1/certification/${wallet}`
const standardsUrl = `${safeOrigin}/v1/score/erc8004?wallet=${wallet}`
const evaluatorUrl = `${safeOrigin}/v1/score/evaluator?wallet=${wallet}`
- const directoryUrl = `${safeOrigin}/v1/certification/directory`
+ const directoryUrl = `${safeOrigin}/directory`
const certifyUrl = `${safeOrigin}/certify?wallet=${wallet}`
const pageUrl = `${safeOrigin}/agent/${wallet}`
const s = score?.composite_score ?? 0
diff --git a/src/templates/certify.ts b/src/templates/certify.ts
index 4804ce2..9d9b13a 100644
--- a/src/templates/certify.ts
+++ b/src/templates/certify.ts
@@ -2,8 +2,9 @@ import { buildPublicUrl } from '../config/public.js'
export function certifyPageHtml(): string {
const certifyUrl = buildPublicUrl('/certify')
- const directoryUrl = buildPublicUrl('/v1/certification/directory')
+ const directoryUrl = buildPublicUrl('/directory')
const readinessUrl = buildPublicUrl('/v1/certification/readiness')
+ const reviewUrl = buildPublicUrl('/v1/certification/review')
const pricingUrl = buildPublicUrl('/pricing')
const docsUrl = buildPublicUrl('/docs')
const explorerUrl = buildPublicUrl('/explorer')
@@ -108,6 +109,14 @@ nav{max-width:1080px;margin:0 auto;padding:0 32px;height:64px;display:flex;align
.readiness-links{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
.readiness-link{display:inline-flex;align-items:center;gap:6px;padding:10px 12px;border-radius:10px;background:var(--bg);border:1px solid var(--border);color:var(--text);font-size:12px;text-decoration:none}
.readiness-link:hover{border-color:var(--border-hi);color:var(--accent);text-decoration:none}
+.readiness-action-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
+.readiness-action-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 14px;border-radius:10px;background:var(--accent);border:none;color:var(--bg);font-size:12px;font-weight:700;cursor:pointer}
+.readiness-action-btn:disabled{opacity:.45;cursor:not-allowed}
+.readiness-review{margin-top:14px;background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:16px}
+.readiness-review-head{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:10px}
+.readiness-review-title{font-size:14px;font-weight:700}
+.readiness-review-note{font-size:12px;color:var(--text-dim);line-height:1.7}
+.readiness-review-meta{margin-top:10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-muted);line-height:1.8}
.readiness-code{margin-top:14px;background:#08101d;border:1px solid var(--border);border-radius:12px;padding:16px;overflow:auto}
.readiness-code-label{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
.readiness-code pre{font-family:'JetBrains Mono',monospace;font-size:12px;line-height:1.7;color:var(--text-dim);white-space:pre-wrap}
@@ -268,6 +277,14 @@ footer{border-top:1px solid var(--border);padding:36px 0 48px;margin-top:72px}
Free
GET
+