Skip to content

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:

json
{
  "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.

Returns collections ordered by recent generation activity with their featured deck and outputs. Used for the main homepage gallery.

GET /miniapp/collections/trending

Query Parameters:

ParameterTypeDefaultDescription
limitnumber20Max collections per page (1-100)
afterstringnullCursor for pagination
outputs_per_decknumber6Max outputs to include per deck
trending_hoursnumber48Time window for trending calculation
excludestringnullComma-separated collection IDs to exclude

Response (200):

json
{
  "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:

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

Returns a single featured collection for the homepage hero section.

GET /miniapp/collections/featured

Query Parameters:

ParameterTypeDefaultDescription
outputs_per_decknumber6Max outputs to include

Response (200):

json
{
  "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/outputs

Path Parameters:

ParameterTypeDescription
deckIduuidThe deck ID

Query Parameters:

ParameterTypeDefaultDescription
limitnumber20Max outputs per page (1-100)
afterstringnullCursor for pagination

Response (200):

json
{
  "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).

typescript
import { getCollectionDisplayImage } from "@/hooks/queries";

const imageUrl = getCollectionDisplayImage(collection);
// Returns: thumbnail_url > url > cover_image

isVideoOutput ​

Check if an output is a video with Mux streaming support.

typescript
import { isVideoOutput } from "@/hooks/queries";

if (isVideoOutput(output)) {
  // Use MuxPlayer component
}

getMuxStreamUrl ​

Get the HLS stream URL for Mux video playback.

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

typescript
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 SectionEndpoints Used
Homepage Hero/miniapp/collections/featured
Homepage Gallery/miniapp/collections/trending
Collection Detail/miniapp/decks/:deckId/outputs
Infinite ScrollAll endpoints (cursor pagination)

Architecture ​

Data Flow ​

Database Query Strategy ​

The miniapp API uses optimized raw SQL queries with Common Table Expressions (CTEs) for aggregation:

sql
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 LAST

This approach ensures:

  • Single round-trip to database
  • Efficient aggregation in the database layer
  • Minimal data transfer to application

Error Codes ​

StatusMeaning
400Bad Request - Invalid parameters
404Not Found - Collection or deck doesn't exist
500Internal Server Error

Migration from Local Queries ​

The miniapp previously used direct Prisma queries in Next.js API routes. The new architecture:

BeforeAfter
/api/collections (local)/miniapp/collections/trending (emprops-api)
Direct Prisma queriesRaw SQL via backend API
Offset-based paginationCursor-based pagination
Multiple round-tripsSingle optimized query

Why This Change? ​

  1. Speed: Raw SQL with CTEs is faster than ORM abstractions
  2. Separation: Backend handles complex queries, frontend stays thin
  3. Caching: Backend can implement caching strategies
  4. Scaling: API can be scaled independently of frontend

Released under the MIT License.