openapi: 3.1.0
info:
  title: Orrery Public API
  version: 0.3.0
  description: |
    Public, CORS-enabled JSON API for Orrery (orrery.me) — a read-only
    intelligence terminal for prediction markets. Every endpoint is open
    and rate-limit-friendly. No authentication required for read paths.

    Source-of-truth links:
      • llms.txt:           https://orrery.me/llms.txt
      • llms-full.txt:      https://orrery.me/llms-full.txt
      • knowledge graph:    https://orrery.me/knowledge-graph.jsonld
      • agent manifest:     https://orrery.me/.well-known/ai-plugin.json
  contact:
    name: Orrery
    email: hello@orrery.me
    url: https://orrery.me/contact
  license:
    name: Public read-only data — see https://orrery.me/terms
servers:
  - url: https://orrery.me
    description: Production
tags:
  - name: markets
    description: Prediction-market data
  - name: search
    description: Discovery
  - name: wallets
    description: On-chain wallet intelligence
  - name: ai
    description: AI explanations and research copilot
  - name: brief
    description: Daily Brief subscriptions and delivery
paths:
  /api/markets:
    get:
      tags: [markets]
      summary: Batch market lookup by slug
      description: |
        Returns full market objects for up to 50 slugs at once. Used by the
        Watchlist and any external consumer that already knows the slugs.
      parameters:
        - in: query
          name: slugs
          required: false
          schema: { type: string }
          description: Comma-separated list of slugs. Max 50.
          example: bitcoin-above-78k-on-april-24,will-trump-win-2028
      responses:
        '200':
          description: Markets with timestamps.
          content:
            application/json:
              schema:
                type: object
                properties:
                  markets:
                    type: array
                    items: { $ref: '#/components/schemas/OrreryMarket' }
                  fetchedAt:
                    type: string
                    format: date-time
  /api/search:
    get:
      tags: [search, markets]
      summary: Live market search
      description: |
        Full-text search across the top 300 markets by 24h volume. Used by
        the Orrery command palette (⌘K). Returns up to 20 matches.
      parameters:
        - in: query
          name: q
          required: false
          schema: { type: string }
          description: Search text. Empty returns the top 20 by volume.
      responses:
        '200':
          description: Search results.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        kind: { type: string, enum: [market] }
                        slug: { type: string }
                        question: { type: string }
                        category: { type: string }
                        price: { type: number }
                        priceChange24h: { type: number }
                        volume24h: { type: number }
  /api/wallets:
    get:
      tags: [wallets]
      summary: Wallet positions, activity, identity
      parameters:
        - in: query
          name: address
          required: true
          schema:
            type: string
            pattern: '^0x[a-fA-F0-9]{40}$'
          description: Ethereum-style proxy wallet.
      responses:
        '200':
          description: Wallet bundle.
          content:
            application/json:
              schema:
                type: object
                properties:
                  address: { type: string }
                  positions:
                    type: array
                    items: { $ref: '#/components/schemas/OrreryPosition' }
                  activity:
                    type: array
                    items: { $ref: '#/components/schemas/DataApiActivity' }
                  pseudonym: { type: string, nullable: true }
                  fetchedAt: { type: string, format: date-time }
        '400':
          description: Address must match `^0x[a-fA-F0-9]{40}$`.
  /api/explain:
    post:
      tags: [ai, markets]
      summary: Streaming "why did it move?" explanation
      description: |
        Streams a concise, source-cited explanation of the supplied market's
        recent move. When `ANTHROPIC_API_KEY` is configured server-side, the
        backend is `claude-sonnet-4-6` with prompt caching on the system
        prompt; otherwise a deterministic heuristic is returned. The
        backend signs which path was used via the
        `x-orrery-explain-source` response header.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExplainRequest' }
      responses:
        '200':
          description: Streaming text/plain. The body is appended to as the
            model emits tokens.
          headers:
            x-orrery-explain-source:
              schema:
                type: string
                enum: [claude-sonnet-4-6, heuristic]
              description: Generation backend that produced this response.
          content:
            text/plain:
              schema: { type: string }
        '400':
          description: Missing or invalid body. `question` is required.
  /api/copilot:
    post:
      tags: [ai]
      summary: Streaming AI research answer
      description: |
        Free-text Q&A grounded in a live snapshot of the top 60 markets +
        active signals. The system prompt forbids predictions and "insider"
        framing; every claim must cite a specific market.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [question]
              properties:
                question:
                  type: string
                  maxLength: 1000
      responses:
        '200':
          description: Streaming text/plain answer.
          headers:
            x-orrery-copilot-source:
              schema:
                type: string
                enum: [claude-sonnet-4-6, heuristic]
          content:
            text/plain:
              schema: { type: string }
  /api/brief/subscribe:
    post:
      tags: [brief]
      summary: Subscribe email to the Daily Brief
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                source: { type: string, maxLength: 64 }
      responses:
        '200':
          description: Persisted (when KV is configured) or logged as a
            durable record.
        '400':
          description: Invalid email.
  /api/brief/deliver:
    post:
      tags: [brief]
      summary: Cron-triggered Daily Brief delivery
      description: |
        Calls Resend if `RESEND_API_KEY` is configured. Returns a dry-run
        response otherwise. Auth via `Authorization: Bearer ${CRON_SECRET}`
        when `CRON_SECRET` is set.
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Delivery summary or dry-run.
        '401':
          description: Unauthorized when CRON_SECRET is set and not provided.
  /api/waitlist:
    post:
      tags: [brief]
      summary: Generic waitlist capture
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                source: { type: string }
      responses:
        '200': { description: Recorded. }
        '400': { description: Invalid email. }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  schemas:
    OrreryMarket:
      type: object
      required: [id, slug, question, price]
      properties:
        id: { type: string }
        slug: { type: string }
        question: { type: string }
        price:
          type: number
          description: YES probability, 0..1
        priceChange1h: { type: number }
        priceChange24h: { type: number }
        priceChange1w: { type: number }
        volume: { type: number }
        volume24h: { type: number }
        liquidity: { type: number }
        spread: { type: number, nullable: true }
        category: { type: string }
        categorySlug: { type: string }
        endDate: { type: string, nullable: true, format: date-time }
        closed: { type: boolean }
        icon: { type: string, nullable: true }
        image: { type: string, nullable: true }
        yesTokenId: { type: string, nullable: true }
        noTokenId: { type: string, nullable: true }
        conditionId: { type: string, nullable: true }
        description: { type: string, nullable: true }
        resolutionSource: { type: string, nullable: true }
        umaResolutionStatus: { type: string, nullable: true }
        negRisk: { type: boolean }
        momentumScore: { type: number, description: '0..100 heuristic' }
        updatedAt: { type: string, nullable: true, format: date-time }
    OrreryPosition:
      type: object
      properties:
        address: { type: string }
        conditionId: { type: string }
        slug: { type: string }
        title: { type: string }
        icon: { type: string, nullable: true }
        outcome: { type: string }
        outcomeIndex: { type: integer }
        size: { type: number }
        avgPrice: { type: number }
        curPrice: { type: number }
        currentValue: { type: number }
        cashPnl: { type: number }
        percentPnl: { type: number }
        realizedPnl: { type: number }
        percentRealizedPnl: { type: number }
        endDate: { type: string, nullable: true }
        redeemable: { type: boolean }
    DataApiActivity:
      type: object
      properties:
        proxyWallet: { type: string }
        timestamp: { type: integer }
        conditionId: { type: string }
        type: { type: string, description: 'TRADE | REDEEM | …' }
        size: { type: number }
        usdcSize: { type: number }
        transactionHash: { type: string }
        price: { type: number }
        side: { type: string }
        outcomeIndex: { type: integer }
        title: { type: string }
        slug: { type: string }
        outcome: { type: string }
        pseudonym: { type: string }
    ExplainRequest:
      type: object
      required: [question]
      properties:
        slug: { type: string }
        question: { type: string }
        category: { type: string }
        price: { type: number }
        priceChange1h: { type: number }
        priceChange24h: { type: number }
        priceChange1w: { type: number }
        volume24h: { type: number }
        liquidity: { type: number }
        spread: { type: number, nullable: true }
        endDate: { type: string, nullable: true }
        umaResolutionStatus: { type: string, nullable: true }
        trades:
          type: array
          items:
            type: object
            properties:
              side: { type: string, enum: [BUY, SELL] }
              outcome: { type: string }
              usdValue: { type: number }
              price: { type: number }
              timestamp: { type: integer }
              pseudonym: { type: string, nullable: true }
