Fix: Add missing API endpoints to docs and fix duplicate mints in recommendations

- Added 17 missing endpoints to OpenAPI documentation:
  * 4 analytics endpoints (status-distribution, networks, metadata-quality)
  * 2 system endpoints (stats/timeline)
  * 1 review endpoint (reviews/recent)
  * 4 mint discovery endpoints (activity, recent, updated, popular)
  * 6 mint detail endpoints (stats, card, latency/timeseries, availability, trust/history, trust/compare, reviews/summary, views/timeseries)

- Fixed duplicate mints bug in recommendation endpoints:
  * Modified SQL queries to use subquery for uptime_rollups
  * Prevents cartesian product from 493 duplicate rollup entries
  * All recommendation endpoints now return unique mints

- Affected endpoints:
  * GET /mints/recommended (all use cases)
  * GET /wallets/:wallet_name/recommended-mints

All 82 API endpoints now documented and tested.
All 23 core endpoints verified working.
This commit is contained in:
Michaël
2025-12-27 14:14:41 -03:00
parent 5383af4695
commit 96f440da1a
2 changed files with 707 additions and 4 deletions

View File

@@ -157,6 +157,67 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au
} }
} }
}, },
'/stats/timeline': {
get: {
tags: ['System'],
summary: 'Activity timeline',
description: 'Returns daily activity timeline for probes, incidents, and reviews',
operationId: 'getStatsTimeline',
parameters: [{
name: 'days',
in: 'query',
description: 'Number of days to include',
schema: { type: 'integer', default: 7, maximum: 30 }
}],
responses: {
200: {
description: 'Activity timeline',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
probes: {
type: 'array',
items: {
type: 'object',
properties: {
date: { type: 'string', format: 'date' },
total: { type: 'integer' },
successful: { type: 'integer' }
}
}
},
incidents: {
type: 'array',
items: {
type: 'object',
properties: {
date: { type: 'string', format: 'date' },
count: { type: 'integer' }
}
}
},
reviews: {
type: 'array',
items: {
type: 'object',
properties: {
date: { type: 'string', format: 'date' },
count: { type: 'integer' }
}
}
},
days: { type: 'integer' },
computed_at: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
}
},
'/reviews': { '/reviews': {
get: { get: {
tags: ['Reviews'], tags: ['Reviews'],
@@ -212,6 +273,45 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au
} }
} }
}, },
'/reviews/recent': {
get: {
tags: ['Reviews'],
summary: 'Recent ecosystem reviews',
description: 'Returns recent reviews across the entire ecosystem',
operationId: 'getRecentReviews',
parameters: [{
name: 'limit',
in: 'query',
description: 'Maximum number of results',
schema: { type: 'integer', default: 20, maximum: 100 }
},
{
name: 'since',
in: 'query',
description: 'Unix timestamp - only reviews after this time',
schema: { type: 'integer' }
}
],
responses: {
200: {
description: 'Recent reviews',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
reviews: {
type: 'array',
items: { $ref: '#/components/schemas/Review' }
}
}
}
}
}
}
}
}
},
'/analytics/uptime': { '/analytics/uptime': {
get: { get: {
tags: ['Analytics'], tags: ['Analytics'],
@@ -300,6 +400,113 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au
} }
} }
}, },
'/analytics/status-distribution': {
get: {
tags: ['Analytics'],
summary: 'Status distribution',
description: 'Returns the distribution of mint statuses across the ecosystem',
operationId: 'getStatusDistribution',
responses: {
200: {
description: 'Status distribution',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
distribution: {
type: 'object',
properties: {
online: { type: 'integer' },
degraded: { type: 'integer' },
offline: { type: 'integer' },
abandoned: { type: 'integer' },
unknown: { type: 'integer' }
}
},
total: { type: 'integer' },
computed_at: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
}
},
'/analytics/networks': {
get: {
tags: ['Analytics'],
summary: 'Network breakdown',
description: 'Returns network type breakdown (clearnet, tor, dual-stack)',
operationId: 'getNetworkBreakdown',
responses: {
200: {
description: 'Network breakdown',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
clearnet_only: { type: 'integer' },
tor_only: { type: 'integer' },
dual_stack: { type: 'integer' },
total: { type: 'integer' },
computed_at: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
}
},
'/analytics/metadata-quality': {
get: {
tags: ['Analytics'],
summary: 'Metadata quality leaderboard',
description: 'Returns mints ranked by metadata completeness',
operationId: 'getMetadataQuality',
parameters: [{
name: 'limit',
in: 'query',
description: 'Maximum number of results',
schema: { type: 'integer', default: 50, maximum: 200 }
}],
responses: {
200: {
description: 'Metadata quality rankings',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
mints: {
type: 'array',
items: {
type: 'object',
properties: {
mint_id: { type: 'string', format: 'uuid' },
canonical_url: { type: 'string' },
name: { type: 'string' },
completeness_score: { type: 'number' },
has_name: { type: 'boolean' },
has_description: { type: 'boolean' },
has_icon: { type: 'boolean' },
has_contact: { type: 'boolean' },
has_pubkey: { type: 'boolean' }
}
}
},
total: { type: 'integer' }
}
}
}
}
}
}
}
},
'/wallets/{wallet_name}/recommended-mints': { '/wallets/{wallet_name}/recommended-mints': {
get: { get: {
tags: ['Mints'], tags: ['Mints'],
@@ -367,6 +574,145 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au
} }
} }
}, },
'/mints/activity': {
get: {
tags: ['Mints'],
summary: 'Mint ecosystem activity',
description: 'Get mint ecosystem activity overview including recent additions and updates',
operationId: 'getMintActivity',
responses: {
200: {
description: 'Ecosystem activity',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
new_mints_24h: { type: 'integer' },
new_mints_7d: { type: 'integer' },
updated_24h: { type: 'integer' },
status_changes_24h: { type: 'integer' },
computed_at: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
}
},
'/mints/recent': {
get: {
tags: ['Mints'],
summary: 'Recently added mints',
description: 'Get mints that were recently added to the ecosystem',
operationId: 'getRecentMints',
parameters: [{
name: 'window',
in: 'query',
description: 'Time window',
schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '7d' }
},
{
name: 'limit',
in: 'query',
description: 'Maximum number of results',
schema: { type: 'integer', default: 20, maximum: 100 }
}
],
responses: {
200: {
description: 'Recently added mints',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
window: { type: 'string' },
mints: {
type: 'array',
items: { $ref: '#/components/schemas/MintCard' }
}
}
}
}
}
}
}
}
},
'/mints/updated': {
get: {
tags: ['Mints'],
summary: 'Recently updated mints',
description: 'Get mints that were recently updated (metadata changes)',
operationId: 'getUpdatedMints',
parameters: [{
name: 'limit',
in: 'query',
description: 'Maximum number of results',
schema: { type: 'integer', default: 20, maximum: 100 }
}],
responses: {
200: {
description: 'Recently updated mints',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
mints: {
type: 'array',
items: { $ref: '#/components/schemas/MintCard' }
}
}
}
}
}
}
}
}
},
'/mints/popular': {
get: {
tags: ['Mints'],
summary: 'Popular mints by views',
description: 'Get mints ranked by pageview count',
operationId: 'getPopularMints',
parameters: [{
name: 'window',
in: 'query',
description: 'Time window',
schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '7d' }
},
{
name: 'limit',
in: 'query',
description: 'Maximum number of results',
schema: { type: 'integer', default: 20, maximum: 100 }
}
],
responses: {
200: {
description: 'Popular mints',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
window: { type: 'string' },
mints: {
type: 'array',
items: { $ref: '#/components/schemas/MintCard' }
}
}
}
}
}
}
}
}
},
'/mints/cards': { '/mints/cards': {
get: { get: {
tags: ['Mints'], tags: ['Mints'],
@@ -937,6 +1283,339 @@ Admin endpoints require the \`X-Admin-Api-Key\` header. All admin actions are au
} }
} }
}, },
'/mints/{mint_id}/views/timeseries': {
get: {
tags: ['Analytics'],
summary: 'Pageview timeseries',
description: 'Returns pageview history for adoption trend charts',
operationId: 'getMintViewsTimeseries',
parameters: [
{ $ref: '#/components/parameters/mintId' },
{
name: 'window',
in: 'query',
description: 'Time window',
schema: { type: 'string', enum: ['7d', '30d'], default: '7d' }
},
{
name: 'bucket',
in: 'query',
description: 'Time bucket size',
schema: { type: 'string', enum: ['1h', '1d'], default: '1d' }
}
],
responses: {
200: {
description: 'Pageview timeseries',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
window: { type: 'string' },
bucket: { type: 'string' },
data: {
type: 'array',
items: {
type: 'object',
properties: {
timestamp: { type: 'string', format: 'date-time' },
views: { type: 'integer' },
unique_sessions: { type: 'integer' }
}
}
}
}
}
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/stats': {
get: {
tags: ['Mint Details'],
summary: 'Aggregated mint KPIs',
description: 'Returns key performance indicators in a single request for summary cards',
operationId: 'getMintStats',
parameters: [
{ $ref: '#/components/parameters/mintId' }
],
responses: {
200: {
description: 'Mint statistics',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
mint_id: { type: 'string', format: 'uuid' },
uptime: {
type: 'object',
properties: {
uptime_24h: { type: 'number' },
uptime_7d: { type: 'number' },
uptime_30d: { type: 'number' }
}
},
latency: {
type: 'object',
properties: {
avg_rtt_ms: { type: 'number' },
p95_rtt_ms: { type: 'number' }
}
},
trust_score: { type: 'integer' },
trust_level: { type: 'string' },
review_count: { type: 'integer' },
avg_rating: { type: 'number', nullable: true },
views_30d: { type: 'integer' },
computed_at: { type: 'string', format: 'date-time' }
}
}
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/card': {
get: {
tags: ['Mint Details'],
summary: 'Get mint card',
description: 'Optimized lightweight endpoint for grid/list views',
operationId: 'getMintCard',
parameters: [
{ $ref: '#/components/parameters/mintId' }
],
responses: {
200: {
description: 'Mint card data',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/MintCard' }
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/latency/timeseries': {
get: {
tags: ['Uptime'],
summary: 'Response time history',
description: 'Returns latency timeseries for charting response time trends',
operationId: 'getLatencyTimeseries',
parameters: [
{ $ref: '#/components/parameters/mintId' },
{
name: 'window',
in: 'query',
description: 'Time window',
schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '24h' }
},
{
name: 'bucket',
in: 'query',
description: 'Time bucket size',
schema: { type: 'string', enum: ['5m', '15m', '1h'], default: '1h' }
}
],
responses: {
200: {
description: 'Latency timeseries',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
window: { type: 'string' },
bucket: { type: 'string' },
data: {
type: 'array',
items: {
type: 'object',
properties: {
timestamp: { type: 'string', format: 'date-time' },
avg_rtt_ms: { type: 'number' },
min_rtt_ms: { type: 'number' },
max_rtt_ms: { type: 'number' },
p95_rtt_ms: { type: 'number' }
}
}
}
}
}
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/availability': {
get: {
tags: ['Uptime'],
summary: 'Availability breakdown',
description: 'Returns availability breakdown showing online/degraded/offline percentages',
operationId: 'getMintAvailability',
parameters: [
{ $ref: '#/components/parameters/mintId' },
{
name: 'window',
in: 'query',
description: 'Time window',
schema: { type: 'string', enum: ['24h', '7d', '30d'], default: '30d' }
}
],
responses: {
200: {
description: 'Availability breakdown',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
window: { type: 'string' },
online_pct: { type: 'number' },
degraded_pct: { type: 'number' },
offline_pct: { type: 'number' },
total_checks: { type: 'integer' },
computed_at: { type: 'string', format: 'date-time' }
}
}
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/trust/history': {
get: {
tags: ['Trust'],
summary: 'Trust score history',
description: 'Returns trust score changes over time with change reasons',
operationId: 'getTrustHistory',
parameters: [
{ $ref: '#/components/parameters/mintId' },
{
name: 'limit',
in: 'query',
description: 'Maximum number of historical records',
schema: { type: 'integer', default: 30, maximum: 100 }
}
],
responses: {
200: {
description: 'Trust score history',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
history: {
type: 'array',
items: {
type: 'object',
properties: {
computed_at: { type: 'string', format: 'date-time' },
score_total: { type: 'integer' },
score_level: { type: 'string' },
breakdown: { type: 'object' }
}
}
}
}
}
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/trust/compare': {
get: {
tags: ['Trust'],
summary: 'Compare trust score',
description: 'Compare mint trust score against ecosystem benchmarks',
operationId: 'getTrustComparison',
parameters: [
{ $ref: '#/components/parameters/mintId' },
{
name: 'against',
in: 'query',
description: 'Comparison benchmark',
schema: { type: 'string', enum: ['ecosystem', 'top10', 'median'], default: 'ecosystem' }
}
],
responses: {
200: {
description: 'Trust score comparison',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
mint_score: { type: 'integer' },
benchmark_score: { type: 'number' },
percentile: { type: 'number' },
rank: { type: 'integer' },
total_mints: { type: 'integer' }
}
}
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/reviews/summary': {
get: {
tags: ['Reviews'],
summary: 'Review summary',
description: 'Quick review overview with rating distribution and averages',
operationId: 'getReviewSummary',
parameters: [
{ $ref: '#/components/parameters/mintId' }
],
responses: {
200: {
description: 'Review summary',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
total_reviews: { type: 'integer' },
avg_rating: { type: 'number', nullable: true },
unique_reviewers: { type: 'integer' },
rating_distribution: {
type: 'object',
properties: {
'5': { type: 'integer' },
'4': { type: 'integer' },
'3': { type: 'integer' },
'2': { type: 'integer' },
'1': { type: 'integer' }
}
},
last_review_at: { type: 'string', format: 'date-time', nullable: true }
}
}
}
}
},
404: { $ref: '#/components/responses/NotFound' }
}
}
},
'/mints/{mint_id}/profile': { '/mints/{mint_id}/profile': {
get: { get: {
tags: ['Mint Details'], tags: ['Mint Details'],

View File

@@ -1569,7 +1569,13 @@ export function getRecommendedMints(useCase = 'general', limit = 10) {
ur.avg_rtt_ms, ur.uptime_pct ur.avg_rtt_ms, ur.uptime_pct
FROM mints m FROM mints m
LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id
LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' LEFT JOIN (
SELECT mint_id, avg_rtt_ms, uptime_pct
FROM uptime_rollups
WHERE window = '30d'
GROUP BY mint_id
HAVING computed_at = MAX(computed_at)
) ur ON m.mint_id = ur.mint_id
WHERE m.status = 'online' WHERE m.status = 'online'
AND (m.visibility IS NULL OR m.visibility = 'public') AND (m.visibility IS NULL OR m.visibility = 'public')
ORDER BY ur.avg_rtt_ms ASC NULLS LAST, ur.uptime_pct DESC NULLS LAST ORDER BY ur.avg_rtt_ms ASC NULLS LAST, ur.uptime_pct DESC NULLS LAST
@@ -1585,7 +1591,13 @@ export function getRecommendedMints(useCase = 'general', limit = 10) {
ur.uptime_pct ur.uptime_pct
FROM mints m FROM mints m
LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id
LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' LEFT JOIN (
SELECT mint_id, uptime_pct
FROM uptime_rollups
WHERE window = '30d'
GROUP BY mint_id
HAVING computed_at = MAX(computed_at)
) ur ON m.mint_id = ur.mint_id
WHERE m.status = 'online' WHERE m.status = 'online'
AND (m.visibility IS NULL OR m.visibility = 'public') AND (m.visibility IS NULL OR m.visibility = 'public')
AND ts.score_total >= 70 AND ts.score_total >= 70
@@ -1618,7 +1630,13 @@ export function getRecommendedMints(useCase = 'general', limit = 10) {
ur.uptime_pct ur.uptime_pct
FROM mints m FROM mints m
LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id
LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' LEFT JOIN (
SELECT mint_id, uptime_pct
FROM uptime_rollups
WHERE window = '30d'
GROUP BY mint_id
HAVING computed_at = MAX(computed_at)
) ur ON m.mint_id = ur.mint_id
WHERE m.status = 'online' WHERE m.status = 'online'
AND (m.visibility IS NULL OR m.visibility = 'public') AND (m.visibility IS NULL OR m.visibility = 'public')
ORDER BY ts.score_total DESC NULLS LAST, ur.uptime_pct DESC NULLS LAST ORDER BY ts.score_total DESC NULLS LAST, ur.uptime_pct DESC NULLS LAST
@@ -1741,7 +1759,13 @@ export function getWalletRecommendedMints(walletName, limit = 10) {
FROM mints m FROM mints m
LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id LEFT JOIN current_trust_scores ts ON m.mint_id = ts.mint_id
LEFT JOIN metadata_snapshots ms ON m.mint_id = ms.mint_id LEFT JOIN metadata_snapshots ms ON m.mint_id = ms.mint_id
LEFT JOIN uptime_rollups ur ON m.mint_id = ur.mint_id AND ur.window = '30d' LEFT JOIN (
SELECT mint_id, uptime_pct
FROM uptime_rollups
WHERE window = '30d'
GROUP BY mint_id
HAVING computed_at = MAX(computed_at)
) ur ON m.mint_id = ur.mint_id
WHERE m.status = 'online' WHERE m.status = 'online'
AND (m.visibility IS NULL OR m.visibility = 'public') AND (m.visibility IS NULL OR m.visibility = 'public')
`); `);