Miniapp API
Public API endpoints served by emprops-api for the Emerge Miniapp frontend. These endpoints replace direct database queries, providing optimized data fetching with proper pagination and efficient SQL.
Overview
The Miniapp API is designed for speed and optimized for the homepage experience:
- Cursor-based pagination for infinite scroll
- Raw SQL with CTEs for fast aggregation queries
- Configurable trending windows for relevance tuning
- Mux video support with playback IDs for streaming
Base URL
NEXT_PUBLIC_EMPROPS_API_BASE_URL=/miniapp/...Response Format
All endpoints return responses in this format:
{
"data": [ /* array of items */ ],
"cursor": {
"next": "cursor-string-or-null",
"has_more": true
},
"error": null | "Error message"
}Homepage Endpoints
These endpoints power the Emerge Miniapp homepage.
Get Trending Collections
Returns collections ordered by recent generation activity with their featured deck and outputs. Used for the main homepage gallery.
GET /miniapp/collections/trendingQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Max collections per page (1-100) |
after | string | null | Cursor for pagination |
outputs_per_deck | number | 6 | Max outputs to include per deck |
trending_hours | number | 48 | Time window for trending calculation |
exclude | string | null | Comma-separated collection IDs to exclude |
Response (200):
{
"data": [
{
"id": "uuid",
"title": "Veo Video Generator",
"description": "Create stunning AI videos",
"cover_image": "https://cdn.example.com/cover.jpg",
"creator": {
"fid": "12345",
"username": "creator.eth",
"pfp": "https://..."
},
"config": {
"price": 0.001,
"is_featured": false,
"generations_per_payment": 1
},
"featured_deck": {
"id": "uuid",
"name": "Recent Generations",
"output_count": 45,
"has_more_outputs": true,
"outputs": [
{
"id": "uuid",
"url": "https://cdn.example.com/image.jpg",
"thumbnail_url": "https://cdn.example.com/thumb.jpg",
"mime_type": "image/jpeg",
"status": "completed",
"created_at": "2024-01-10T12:00:00Z",
"parent_output_id": null,
"mux_playback_id": null,
"mux_asset_id": null,
"creator": {
"fid": "67890",
"username": "user.eth",
"pfp": "https://..."
}
}
]
},
"total_decks": 12,
"total_outputs": 156,
"recent_outputs": 23
}
],
"cursor": {
"next": "eyJsYXN0X2lkIjoiYWJjMTIzIn0",
"has_more": true
},
"error": null
}Frontend Hook:
import { useTrendingCollections } from "@/hooks/queries";
const {
collections, // Flattened array from all pages
isLoading,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useTrendingCollections({
limit: 20,
outputsPerDeck: 6,
trendingHours: 48,
excludeCollectionIds: [FEATURED_ID],
enabled: true
});Get Featured Collection
Returns a single featured collection for the homepage hero section.
GET /miniapp/collections/featuredQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
outputs_per_deck | number | 6 | Max outputs to include |
Response (200):
{
"data": {
"id": "uuid",
"title": "Featured: AI Art Generator",
"description": "This week's featured collection",
"cover_image": "https://cdn.example.com/featured-cover.jpg",
"creator": {
"fid": "12345",
"username": "emerge.eth",
"pfp": "https://..."
},
"config": {
"price": null,
"is_featured": true,
"generations_per_payment": 3
},
"featured_deck": {
"id": "uuid",
"name": "Showcase",
"output_count": 100,
"has_more_outputs": true,
"outputs": [ /* ... */ ]
},
"total_decks": 1,
"total_outputs": 100,
"recent_outputs": 15
},
"error": null
}Deck Endpoints
Get Deck Outputs
Returns paginated outputs for a specific deck with cursor-based pagination.
GET /miniapp/decks/:deckId/outputsPath Parameters:
| Parameter | Type | Description |
|---|---|---|
deckId | uuid | The deck ID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Max outputs per page (1-100) |
after | string | null | Cursor for pagination |
Response (200):
{
"data": {
"deck_id": "uuid",
"deck_name": "My Generations",
"outputs": [
{
"id": "uuid",
"url": "https://cdn.example.com/video.mp4",
"thumbnail_url": "https://cdn.example.com/thumb.jpg",
"mime_type": "video/mp4",
"status": "completed",
"created_at": "2024-01-10T12:00:00Z",
"parent_output_id": "uuid-of-source",
"mux_playback_id": "7iwyZezQo4fjZ...",
"mux_asset_id": "1L3400tF301xX...",
"creator": {
"fid": "12345",
"username": "user.eth",
"pfp": "https://..."
}
}
]
},
"cursor": {
"next": "eyJsYXN0X2lkIjoiZGVmNDU2In0",
"has_more": true
},
"error": null
}Helper Functions
The useTrendingCollections hook exports helper functions for working with outputs:
getCollectionDisplayImage
Get the best display image for a collection (prioritizes deck output over cover).
import { getCollectionDisplayImage } from "@/hooks/queries";
const imageUrl = getCollectionDisplayImage(collection);
// Returns: thumbnail_url > url > cover_imageisVideoOutput
Check if an output is a video with Mux streaming support.
import { isVideoOutput } from "@/hooks/queries";
if (isVideoOutput(output)) {
// Use MuxPlayer component
}getMuxStreamUrl
Get the HLS stream URL for Mux video playback.
import { getMuxStreamUrl } from "@/hooks/queries";
const streamUrl = getMuxStreamUrl(output);
// Returns: "https://stream.mux.com/{playback_id}.m3u8"getMuxThumbnailUrl
Get a thumbnail URL for Mux videos with optional dimensions.
import { getMuxThumbnailUrl } from "@/hooks/queries";
const thumb = getMuxThumbnailUrl(output, { width: 640, height: 360, time: 2 });
// Returns: "https://image.mux.com/{playback_id}/thumbnail.jpg?width=640&height=360&time=2"App Section Mapping
| App Section | Endpoints Used |
|---|---|
| Homepage Hero | /miniapp/collections/featured |
| Homepage Gallery | /miniapp/collections/trending |
| Collection Detail | /miniapp/decks/:deckId/outputs |
| Infinite Scroll | All endpoints (cursor pagination) |
Architecture
Data Flow
Database Query Strategy
The miniapp API uses optimized raw SQL queries with Common Table Expressions (CTEs) for aggregation:
WITH trending_counts AS (
SELECT collection_id, COUNT(*) as recent_count
FROM output
WHERE created_at >= NOW() - INTERVAL '48 hours'
AND status = 'completed'
GROUP BY collection_id
),
featured_decks AS (
SELECT DISTINCT ON (collection_id) *
FROM deck
WHERE status = 'active'
ORDER BY collection_id, output_count DESC
)
SELECT c.*, tc.recent_count, fd.*
FROM collection c
LEFT JOIN trending_counts tc ON c.id = tc.collection_id
LEFT JOIN featured_decks fd ON c.id = fd.collection_id
ORDER BY tc.recent_count DESC NULLS LASTThis approach ensures:
- Single round-trip to database
- Efficient aggregation in the database layer
- Minimal data transfer to application
Error Codes
| Status | Meaning |
|---|---|
| 400 | Bad Request - Invalid parameters |
| 404 | Not Found - Collection or deck doesn't exist |
| 500 | Internal Server Error |
Migration from Local Queries
The miniapp previously used direct Prisma queries in Next.js API routes. The new architecture:
| Before | After |
|---|---|
/api/collections (local) | /miniapp/collections/trending (emprops-api) |
| Direct Prisma queries | Raw SQL via backend API |
| Offset-based pagination | Cursor-based pagination |
| Multiple round-trips | Single optimized query |
Why This Change?
- Speed: Raw SQL with CTEs is faster than ORM abstractions
- Separation: Backend handles complex queries, frontend stays thin
- Caching: Backend can implement caching strategies
- Scaling: API can be scaled independently of frontend
