Skip to content

Image-First Listing — AI Generate Guide

Endpoint: POST /api/listings/ai-generate Status: ✅ Production-tested (Ronda 17, 2026-07-12) Model: anthropic/claude-opus-4.8 via OpenRouter

Overview

Owning.pro supports an image-first listing creation flow. Instead of asking the seller to fill out a form field by field, the user uploads photos of the item they want to sell, and the AI automatically generates a complete listing draft — title, description, category, estimated price range, tags, condition, and category-specific attributes. All fields remain fully editable.

This is the core differentiator of the sell experience: snap photos → get a listing → review → publish.


How it works

User uploads photos
       │
       ▼
POST /api/auth/register (or /login)  ──► JWT token
       │
       ▼
POST /api/listings/ai-generate
   Authorization: Bearer <JWT>
   Body: { "images": ["https://...", "https://..."] }
       │
       ▼
API sends images to OpenRouter (Claude Opus 4.8 vision)
       │
       ▼
Model analyzes images → returns structured JSON draft
       │
       ▼
API validates against schema → returns to client
       │
       ▼
Client pre-fills the sell form (all fields editable)
       │
       ▼
User reviews/edits → POST /api/listings (create listing)

Architecture

Component Location Role
Route services/api/src/routes/listings.ts POST /ai-generate handler
AI Service services/api/src/services/ai-service.ts OpenRouter API call + JSON parsing
Rate Limiter services/api/src/services/rate-limit-service.ts DB-based, 5/hour per user
Schema packages/shared/src/schemas/index.ts Request/response validation (Zod)
Frontend apps/web/components/sell/sell-wizard.tsx 3-step wizard (Photos → Details → Review)

Stub mode

When OPENROUTER_API_KEY is not set, the endpoint runs in stub mode — it returns a mock draft immediately ({ _stub: true, ... }) without calling the AI provider. This allows frontend development without the API key. When the key is added to Railway, the endpoint automatically starts generating real drafts.

Current status: OPENROUTER_API_KEY is set in Railway (verified W022). The endpoint is in active mode — generating real drafts.

Authentication

The endpoint requires a valid JWT token, obtained via registration or login.

Register

curl -X POST https://api.owning.pro/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "seller@example.com",
    "password": "your-password-min-8-chars",
    "name": "Jane Seller"
  }'

Response (201):

{
  "user": {
    "id": "01KXB1YT1DE003JMMX8MQ6VXSY",
    "email": "seller@example.com",
    "name": "Jane Seller",
    "type": "human",
    "role": "user"
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 604800
}

Login (existing users)

curl -X POST https://api.owning.pro/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "seller@example.com", "password": "your-password" }'

AI Generate — Request

POST /api/listings/ai-generate
Authorization: Bearer <JWT token>
Content-Type: application/json

Request body

Field Type Required Description
images string[] (URLs) ✅ Yes Array of image URLs (1–10). Must be publicly accessible — the AI provider downloads them server-side.
curl -X POST https://api.owning.pro/api/listings/ai-generate \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGci..." \
  -d '{
    "images": [
      "https://static.owning.pro/images/my-listing-slug/1.webp",
      "https://static.owning.pro/images/my-listing-slug/2.webp"
    ]
  }'
⚠️ Important: Image URLs must be publicly downloadable. The AI provider (OpenRouter → Anthropic Claude) fetches the images server-side to feed them to the vision model. URLs that return 404 or require authentication will cause a 503 ai_unavailable error. See Known issues below.

AI Generate — Response

Success (200)

{
  "title": "Canon EOS Rebel T3i DSLR Camera with Pentax-M 50mm f/2 Lens",
  "description": "Canon EOS Rebel T3i (600D) digital SLR camera in black, shown with an adapted vintage SMC Pentax-M 50mm f/2 prime lens by Asahi Optical Co. The camera body features Canon's classic ergonomic grip and control layout. The Rebel T3i is an 18MP APS-C DSLR with a vari-angle LCD screen and Full HD video recording, making it a versatile choice for both photography enthusiasts and beginners. The manual-focus Pentax-M 50mm lens delivers a pleasing image character and is mounted via an adapter, ideal for creative and vintage-style shooting. Body shows good used condition with minor wear consistent with normal handling.",
  "category": "electronics",
  "estimatedPriceRange": {
    "min": 150,
    "max": 300,
    "currency": "EUR"
  },
  "tags": ["canon", "dslr", "eosrebelt3i", "pentax", "50mmlens", "camera", "photography", "600d"],
  "condition": "good",
  "attributes": {
    "brand": "Canon",
    "model": "EOS Rebel T3i (600D)",
    "type": "DSLR Camera",
    "color": "Black",
    "lens": "SMC Pentax-M 50mm f/2",
    "lensMount": "Adapted manual focus",
    "sensorType": "APS-C"
  }
}

Response fields

Field Type Constraints Description
title string 5–120 chars Concise, descriptive title (includes brand/model if visible)
description string 20–5000 chars Detailed description based on what's visible in the images
category string enum One of: boats, cars, motorcycles, electronics, fashion, home-garden, real-estate, other
estimatedPriceRange object { min, max, currency } — realistic second-hand market value
estimatedPriceRange.min number ≥ 0 Estimated minimum value in EUR
estimatedPriceRange.max number ≥ 0 Estimated maximum value in EUR
estimatedPriceRange.currency string 3 chars (ISO 4217) Currency code, default EUR
tags string[] 1–10 items, ≤50 chars each Lowercase search tags (brand, model, type, features)
condition string enum One of: new, like_new, good, fair, poor, refurbished
attributes object free-form key-value Category-specific details (e.g. {"brand": "Apple", "model": "iPhone 13"})

Error responses

HTTP Error code When Message example
401 unauthorized Missing or invalid JWT Authentication required
422 validation_error Invalid request body (e.g. empty images array, non-URL values) Zod validation error
429 rate_limited More than 5 generations in the current hour Limit exceeded for ai_generate. Limit: 5/hour. You have used 6 this hour.
503 ai_unavailable OpenRouter call failed (image 404, timeout, API error, no credits) AI service temporarily unavailable

429 — Rate limited

{
  "error": {
    "code": "rate_limited",
    "message": "Limit exceeded for ai_generate. Limit: 5/hour. You have used 6 this hour."
  }
}

503 — AI unavailable

{
  "error": {
    "code": "ai_unavailable",
    "message": "AI service temporarily unavailable"
  }
}

Rate limiting

Action Limit Window Enforcement
ai_generate 5 generations per hour (UTC hour window) DB-based (rate_limit_counters table)
  • The counter is incremented atomically via a PostgreSQL UPSERT on

(user_id, action, window_start) — safe under concurrent requests.

  • The window resets at the top of each UTC hour (not a rolling 60 minutes).
  • The counter increments before the AI generation runs. If the generation

fails (e.g. image 404 → 503), the slot is still consumed.

  • Other rate-limited actions: listing_create (10/day), image_upload (50/day).

Client retry strategy

On 429, wait until the next UTC hour window and retry. On 503, verify the image URLs are publicly accessible, then retry with a different image or fill manually. The frontend sell wizard handles both cases gracefully — on 429 it shows a message and lets the user fill the form manually; on 503 it falls back to manual entry.


AI provider details

Property Value
Provider OpenRouter (openrouter.ai)
Model anthropic/claude-opus-4.8 (resolves to anthropic/claude-4.8-opus-20260528)
API OpenRouter Chat Completions (/api/v1/chat/completions)
Vision Images sent as image_url content blocks
Max tokens 2000
Timeout 30 seconds (AbortController)
Env vars OPENROUTER_API_KEY (required), OPENROUTER_MODEL (default: anthropic/claude-opus-4.8)
Headers Authorization: Bearer <key>, HTTP-Referer: https://owning.pro, X-Title: Owning.pro

System prompt

The model receives a system prompt instructing it to analyze the images and respond with only a valid JSON object matching the response schema (no markdown, no code fences). The response parser strips code fences if present and extracts the first {...} block as a fallback before validating against the Zod schema.

Response time

Typical generation time: 8–12 seconds per request (varies by image count and model load). The frontend shows a loading state during generation.


End-to-end test results (Ronda 17, 2026-07-12)

Test Result
Endpoint deployed (401 without auth, not 404) ✅ Pass
Auth: register → JWT token ✅ Pass (201)
AI generate with public image URL ✅ Pass (200, full structured draft)
Response schema validation (title, description, category, price range, tags, condition, attributes) ✅ Pass — all fields present and valid
Rate limiting: 429 after exceeding 5/hour ✅ Pass (429 with rate_limited code)
OpenRouter key valid + credits ✅ Pass ($299.88 remaining)
Model anthropic/claude-opus-4.8 responds ✅ Pass (text + vision)

Test artifacts saved to: worklog/20260712_owning_assets/

  • ai_generate_response_camera.json — successful 200 response
  • ai_generate_rate_limit_429.json — 429 rate limit response

Known issues

Scraped listing images return 404 on static.owning.pro

Status: Open (separate from AI generate endpoint)

Scraped listings store image URLs like https://static.owning.pro/images/{slug}/1.webp, but these URLs return 404 in production. The static service (services/static/src/index.ts) reads from a Railway volume at /data/images/{slug}/{n}.webp, and the scraped image files are not present on the volume.

Impact on AI generate: When a user passes a scraped listing's image URL to /api/listings/ai-generate, OpenRouter downloads the image server-side, gets a 404, and the generation fails with 503 ai_unavailable. The AI generate endpoint itself works perfectly with any publicly accessible image URL — the issue is in the scraper image pipeline (images not being uploaded to the static volume), not in the AI generate code.

Resolution: This is a scraper/image-pipeline issue, not an AI generate issue. The image upload step in the scraper pipeline needs to actually write files to the static service's Railway volume via POST /admin/upload or POST /admin/upload-batch.


Frontend integration

The sell wizard (apps/web/components/sell/sell-wizard.tsx) implements a 3-step flow:

  1. Photos — drag & drop upload + "Generate listing with AI" button
  2. Details — AI-generated listing, fully editable (AiDetailsStep)
  3. Review — summary + preview, then POST /api/listings + publish

The wizard calls callAiGenerate() which posts to /api/listings/ai-generate with the uploaded image URLs. If the response includes _stub: true (stub mode), fields stay empty with a hint banner. On 429 or 503, it falls back to manual entry.