{
  "openapi": "3.1.0",
  "info": {
    "title": "Demandex API",
    "version": "0.1.0",
    "x-guidance": "Pay-per-call e-commerce demand intelligence (x402, USDC on Base eip155:8453 only). Start with the two FREE endpoints to learn the response shapes: GET /v1/categories (30/min) for the category index and GET /v1/sample/opportunity (10/min) for a real, provably-lagged opportunity card. Paid routes: GET /v1/opportunities/trending ($0.01) top-20 trending teasers; GET /v1/opportunities?category= ($0.02) up to 50 cards in a category; GET /v1/opportunity?id=|slug= ($0.05) one full evidence card; POST /v1/gauge {query} ($0.03) a demand verdict (7-day cached, corpus-only); POST /v1/gauge/live {query} ($0.10) a verdict backed by fresh Reddit research. Call any paid route without payment to receive the x402 402 challenge, sign the exact-scheme USDC authorization for the Base accepts[] rail, then retry with the PAYMENT-SIGNATURE (x402 V2) or X-PAYMENT (legacy) header. You are never charged for an error.",
    "summary": "E-commerce demand intelligence for AI agents — the index of what people want next. Per-call, no signup.",
    "description": "Demandex mines community demand for physical products. It classifies Reddit complaint and intent posts across ~70 communities into demand signals (replacement-part hunts, unmet needs, willingness-to-pay, repeat failures), clusters them into scored opportunity cards with verbatim evidence and permalinks, and answers ad-hoc demand verdicts for any query. A free honest sample and a free category index let an agent learn the shapes before paying; paid tiers return trending teasers, category lists, full evidence cards, and demand verdicts (7-day cached or live research). Physical products only. Evidence is quoted public Reddit posts with provenance (subreddit, upvotes, permalink). Informational only — not a guarantee of any business outcome.",
    "termsOfService": "https://api.demandex.dev/terms.txt",
    "contact": {
      "name": "Demandex API",
      "url": "https://api.demandex.dev",
      "email": "jcislo918@gmail.com"
    }
  },
  "servers": [
    {
      "url": "https://api.demandex.dev",
      "description": "Demandex API public edge"
    }
  ],
  "externalDocs": {
    "description": "Agent-facing reference (llms.txt)",
    "url": "https://api.demandex.dev/llms.txt"
  },
  "tags": [
    {
      "name": "free",
      "description": "No auth, no payment."
    },
    {
      "name": "paid",
      "description": "x402 V2 per-call pricing (USDC on eip155:8453)."
    }
  ],
  "paths": {
    "/v1/categories": {
      "get": {
        "tags": [
          "free"
        ],
        "operationId": "categories",
        "summary": "FREE category index",
        "description": "Free, no payment. Returns the category index — every active category with its opportunity count, top opportunity score, and last-updated time. A cheap change-detection funnel an agent can poll to learn which categories to buy into. Shape: { categories: [{ slug, opportunityCount, topScore, lastUpdated }], generatedAt }.",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {
                    "categories": [
                      {
                        "slug": "audio",
                        "opportunityCount": 12,
                        "topScore": 87.4,
                        "lastUpdated": "2026-07-18T11:30:00.000Z"
                      }
                    ],
                    "generatedAt": "2026-07-18T11:45:00.000Z"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/sample/opportunity": {
      "get": {
        "tags": [
          "free"
        ],
        "operationId": "sample",
        "summary": "FREE sample opportunity card",
        "description": "Free, no payment. Returns one REAL, fixed sample opportunity card in the SAME full shape as the paid GET /v1/opportunity — all score components, productAngle, summary, and verbatim evidence — plus { sample: true, note, cardAsOf }. It is a stable teaching sample (real data) so an agent can learn the exact card shape before paying. Returns 503 corpus_warming only while the index is empty (no cards mined yet).",
        "security": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {
                    "sample": true,
                    "note": "fixed sample card — same shape as paid /v1/opportunity",
                    "cardAsOf": "2026-07-18T09:00:00.000Z",
                    "slug": "sony-wh1000xm5-replacement-headband",
                    "title": "Sony WH-1000XM5 replacement headband",
                    "category": "audio",
                    "score": 87.4
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/opportunities/trending": {
      "get": {
        "tags": [
          "paid"
        ],
        "operationId": "trending",
        "summary": "Trending opportunities (teaser) ($0.01)",
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.010000"
          },
          "protocols": [
            {
              "x402": {}
            }
          ]
        },
        "description": "Paid ($0.01 USDC via x402). Returns the top 20 active opportunity cards by score as TEASERS — { id, slug, title, category, score, signalCount, lastSeen } only. No evidence and no productAngle (buy GET /v1/opportunity for the full card). While the index is still warming and no active cards exist yet, you get a no-charge 503 corpus_warming (never an empty charged list) — poll the free GET /v1/categories to see when cards go live. Compute-first / settle-after — you are not charged on any error. Response carries fetchedAt.",
        "security": [
          {
            "x402": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {
                    "opportunities": [
                      {
                        "id": "ckxxxx",
                        "slug": "sony-wh1000xm5-replacement-headband",
                        "title": "Sony WH-1000XM5 replacement headband",
                        "category": "audio",
                        "score": 87.4,
                        "signalCount": 34,
                        "lastSeen": "2026-07-18T10:00:00.000Z"
                      }
                    ],
                    "fetchedAt": "2026-07-18T11:45:00.000Z",
                    "charged": true
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        }
      }
    },
    "/v1/opportunities": {
      "get": {
        "tags": [
          "paid"
        ],
        "operationId": "list",
        "summary": "Opportunities in a category ($0.02)",
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.020000"
          },
          "protocols": [
            {
              "x402": {}
            }
          ]
        },
        "description": "Paid ($0.02 USDC via x402). Returns up to 50 active opportunity cards in a category, each as a teaser ({ id, slug, title, category, score, signalCount, lastSeen }) plus its summary. Requires ?category=; ?minScore= optionally filters. A missing or unknown category returns a no-charge 400 that lists the valid categories; a valid category with no active cards yet (index still warming) returns a no-charge 503 corpus_warming instead of an empty charged list. Compute-first / settle-after — you are not charged on any error. Response carries fetchedAt.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Category slug (required). Unknown/missing → no-charge 400 listing valid categories."
          },
          {
            "name": "minScore",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Optional minimum composite score (0-100) filter."
          }
        ],
        "security": [
          {
            "x402": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {
                    "category": "audio",
                    "opportunities": [
                      {
                        "id": "ckxxxx",
                        "slug": "sony-wh1000xm5-replacement-headband",
                        "title": "Sony WH-1000XM5 replacement headband",
                        "category": "audio",
                        "score": 87.4,
                        "signalCount": 34,
                        "lastSeen": "2026-07-18T10:00:00.000Z",
                        "summary": "Owners repeatedly seek an aftermarket replacement headband after the OEM band cracks."
                      }
                    ],
                    "fetchedAt": "2026-07-18T11:45:00.000Z",
                    "charged": true
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        }
      }
    },
    "/v1/opportunity": {
      "get": {
        "tags": [
          "paid"
        ],
        "operationId": "card",
        "summary": "Full opportunity card ($0.05)",
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.050000"
          },
          "protocols": [
            {
              "x402": {}
            }
          ]
        },
        "description": "Paid ($0.05 USDC via x402). Returns one full opportunity card by ?id= or ?slug= (exactly one required, else a no-charge 400): all score components (freqScore, intensityScore, wtpScore), productAngle, summary, evidence[] (verbatim quotes with subreddit, upvotes, permalink), builderMomentum, firstSeen, lastSeen, signalCount, distinctAuthors. Compute-first / settle-after — you are not charged when the id/slug is missing, ambiguous, or not found. Response carries fetchedAt.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opportunity id (provide exactly one of id or slug)."
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opportunity slug (provide exactly one of id or slug)."
          }
        ],
        "security": [
          {
            "x402": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {
                    "id": "ckxxxx",
                    "slug": "sony-wh1000xm5-replacement-headband",
                    "title": "Sony WH-1000XM5 replacement headband",
                    "category": "audio",
                    "score": 87.4,
                    "freqScore": 90,
                    "intensityScore": 82,
                    "wtpScore": 88,
                    "productAngle": "Sell a durable aftermarket replacement headband for the Sony WH-1000XM5.",
                    "summary": "Owners repeatedly seek an aftermarket replacement headband after the OEM band cracks.",
                    "signalCount": 34,
                    "distinctAuthors": 29,
                    "firstSeen": "2026-06-01T00:00:00.000Z",
                    "lastSeen": "2026-07-18T10:00:00.000Z",
                    "evidence": [
                      {
                        "quote": "My XM5 headband cracked after 8 months and Sony wants $90 for a repair — where can I just buy the band?",
                        "permalink": "https://reddit.com/r/headphones/comments/abc123",
                        "upvotes": 214,
                        "subreddit": "headphones"
                      }
                    ],
                    "builderMomentum": null,
                    "fetchedAt": "2026-07-18T11:45:00.000Z",
                    "charged": true
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        }
      }
    },
    "/v1/gauge": {
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "gauge",
        "summary": "Demand verdict (cached) ($0.03)",
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.030000"
          },
          "protocols": [
            {
              "x402": {}
            }
          ]
        },
        "description": "Paid ($0.03 USDC via x402). POST { \"query\": \"...\", \"category\"?: \"...\" } to get a demand verdict for any physical-product query. Answers are cached 7 days by normalized query; on a cache miss Demandex matches the query against the mined corpus (signals and cards by keyword) and runs one LLM synthesis call. query must be 3..200 chars. Reads the JSON body first, falling back to ?query=. Verdict shape: { query, verdictScore (0-100), demandTemperature (cold|warm|hot), rationale, topEvidence[] (≤5, each { quote, permalink, upvotes }), relatedOpportunities[] (≤5, each { slug, score }), cache (hit|miss), fetchedAt }. A zero-match query is a real cold verdict (chargeable), not an error. If the LLM is unavailable you get a 503 and are not charged. Compute-first / settle-after.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "The product/demand query to gauge (3..200 chars). Required."
                  },
                  "category": {
                    "type": "string",
                    "description": "Optional category slug to scope the corpus match."
                  }
                },
                "required": [
                  "query"
                ]
              }
            }
          }
        },
        "security": [
          {
            "x402": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {
                    "query": "sony xm5 replacement headband",
                    "verdictScore": 84,
                    "demandTemperature": "hot",
                    "rationale": "Frequent owner complaints about the OEM headband cracking, with buyers actively hunting for aftermarket replacements.",
                    "topEvidence": [
                      {
                        "quote": "My XM5 headband cracked after 8 months and Sony wants $90 for a repair — where can I just buy the band?",
                        "permalink": "https://reddit.com/r/headphones/comments/abc123",
                        "upvotes": 214
                      }
                    ],
                    "relatedOpportunities": [
                      {
                        "slug": "sony-wh1000xm5-replacement-headband",
                        "score": 87.4
                      }
                    ],
                    "cache": "miss",
                    "fetchedAt": "2026-07-18T11:45:00.000Z",
                    "charged": true
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        }
      }
    },
    "/v1/gauge/live": {
      "post": {
        "tags": [
          "paid"
        ],
        "operationId": "gaugeLive",
        "summary": "Demand verdict (live research) ($0.1)",
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.100000"
          },
          "protocols": [
            {
              "x402": {}
            }
          ]
        },
        "description": "Paid ($0.10 USDC via x402). Same body as /v1/gauge — POST { \"query\": \"...\", \"category\"?: \"...\" } (query 3..200 chars; body-first, ?query= fallback). Combines the mined corpus with a LIVE Reddit search run at request time (up to 50 posts, subreddits unrestricted) and one LLM synthesis call. Total compute budget is 45 seconds; on timeout you get a 504 and are not charged. Returns the same verdict shape as /v1/gauge (cache is always miss). A zero-match, empty-search query is a real cold verdict (chargeable), not an error. If the LLM is unavailable you get a 503 and are not charged. Compute-first / settle-after.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "The product/demand query to gauge with live research (3..200 chars). Required."
                  },
                  "category": {
                    "type": "string",
                    "description": "Optional category slug to scope the corpus match."
                  }
                },
                "required": [
                  "query"
                ]
              }
            }
          }
        },
        "security": [
          {
            "x402": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "example": {
                    "query": "sony xm5 replacement headband",
                    "verdictScore": 86,
                    "demandTemperature": "hot",
                    "rationale": "Live Reddit search plus mined signals show sustained, recent demand for an aftermarket XM5 headband.",
                    "topEvidence": [
                      {
                        "quote": "Just found out the XM5 band is glued in — anyone selling a replacement?",
                        "permalink": "https://reddit.com/r/headphones/comments/def456",
                        "upvotes": 58
                      }
                    ],
                    "relatedOpportunities": [
                      {
                        "slug": "sony-wh1000xm5-replacement-headband",
                        "score": 87.4
                      }
                    ],
                    "cache": "miss",
                    "fetchedAt": "2026-07-18T11:45:00.000Z",
                    "charged": true
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "PaymentRequired": {
        "description": "x402 V2 payment challenge. Sign an `exact` USDC authorization for one accepts[] rail, then retry with the PAYMENT-SIGNATURE (V2) or X-PAYMENT (legacy) header. You are not charged for errors or below-threshold answers.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PaymentRequired"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "x402": {
        "type": "http",
        "scheme": "x402",
        "description": "Custom HTTP 402 payment flow (x402 V2). The server answers an unpaid paid-route request with a 402 whose body is a PaymentRequired envelope (x402Version + accepts[] + terms). Each accepts[] entry is an `exact`-scheme USDC rail. Sign ONE rail and retry the same request with the `PAYMENT-SIGNATURE` header (x402 V2 clients) or the legacy `X-PAYMENT` header. Per-call pricing."
      }
    },
    "schemas": {
      "PaymentRequired": {
        "type": "object",
        "description": "x402 V2 PaymentRequired envelope. accepts[] lists one exact USDC rail per network.",
        "properties": {
          "x402Version": {
            "type": "integer",
            "const": 2
          },
          "accepts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "scheme": {
                  "type": "string",
                  "const": "exact"
                },
                "network": {
                  "type": "string",
                  "enum": [
                    "eip155:8453"
                  ]
                },
                "asset": {
                  "type": "string",
                  "const": "USDC"
                },
                "price": {
                  "type": "string",
                  "description": "USD price string."
                },
                "payTo": {
                  "type": "string",
                  "description": "Settlement address for this rail."
                },
                "mimeType": {
                  "type": "string",
                  "const": "application/json"
                }
              }
            }
          },
          "terms": {
            "type": "string",
            "format": "uri"
          }
        }
      }
    }
  }
}
