{"openapi":"3.1.0","info":{"title":"demandgraph","version":"0.1.0","description":"Fuse search, developer, product, employment and commerce signals into demand momentum, acceleration, commercial-intent and durability scores."},"servers":[{"url":"https://web-production-37b02.up.railway.app"}],"paths":{"/health":{"get":{"summary":"Liveness and deployed version","operationId":"getHealth","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/":{"get":{"summary":"Service index — endpoints, auth and error format","operationId":"get/","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/scores":{"post":{"summary":"Score and rank topics by demand momentum","description":"Fuses supplied signals into momentum, acceleration, commercial intent and durability. Quota cost scales with the number of topics, so a 50-topic request bills 50. You supply the observations — DemandGraph does not acquire or resell signal data, which is what keeps it free of licensing and redistribution constraints.","operationId":"postV1Scores","security":[{"apiKey":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"scores":{"type":"array","items":{"type":"object","properties":{"topic":{"type":"string"},"momentum":{"type":"number","minimum":-100,"maximum":100,"description":"Weighted rate of change. A large but flat market scores 0."},"acceleration":{"type":"number","minimum":-100,"maximum":100,"description":"Change in the rate itself — separates \"big\" from \"taking off\"."},"commercialIntent":{"type":"number","minimum":0,"maximum":100,"description":"Share of signal from buy-intent sources rather than read-intent."},"durability":{"type":"number","minimum":0,"maximum":100,"description":"Consistency of direction. A single viral day scores low however large."},"trend":{"type":"string","enum":["accelerating","rising","flat","declining","collapsing"]},"sourceCount":{"type":"integer"},"confidence":{"type":"integer","minimum":0,"maximum":100,"description":"How much the inputs support the score. Discard thin scores rather than acting on them."},"breakdown":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"points":{"type":"integer"},"changePct":{"type":"number"},"weight":{"type":"number"}}}},"windowStart":{"type":"string","format":"date-time"},"windowEnd":{"type":"string","format":"date-time"}}}}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["topics"],"properties":{"topics":{"type":"array","minItems":1,"maxItems":200,"items":{"type":"object","required":["topic","points"],"properties":{"topic":{"type":"string","maxLength":256},"points":{"type":"array","minItems":2,"items":{"type":"object","required":["source","at","value"],"properties":{"source":{"type":"string","enum":["search","developer","product","employment","social","commerce"]},"at":{"type":"string","format":"date-time"},"value":{"type":"number","description":"Unitless. Momentum is computed from relative change, so raw counts are fine."},"weight":{"type":"number","minimum":0,"maximum":1,"description":"Overrides the default weight for this source."}}}}}}},"weights":{"type":"object","description":"Override per-source weights, 0-1. Defaults: commerce 1.0, employment 0.9, developer 0.7, product 0.6, search 0.5, social 0.3."}}},"example":{"topics":[{"topic":"mcp servers","points":[{"source":"search","at":"2026-06-01T00:00:00Z","value":1200},{"source":"search","at":"2026-07-01T00:00:00Z","value":2100},{"source":"search","at":"2026-08-01T00:00:00Z","value":4800},{"source":"developer","at":"2026-06-01T00:00:00Z","value":340},{"source":"developer","at":"2026-07-01T00:00:00Z","value":910},{"source":"developer","at":"2026-08-01T00:00:00Z","value":2400},{"source":"employment","at":"2026-06-01T00:00:00Z","value":12},{"source":"employment","at":"2026-08-01T00:00:00Z","value":47}]}]}}}},"x-quota-cost":0}},"/v1/demo/score":{"post":{"summary":"Public demo — scores one topic without a key","description":"Runs the real scoring engine. Capped at 3 topics and 60 observations. Nothing is stored, nothing is metered.","operationId":"postV1DemoScore","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["topics"],"properties":{"topics":{"type":"array","maxItems":3,"items":{"type":"object","required":["topic","points"],"properties":{"topic":{"type":"string","maxLength":256},"points":{"type":"array","minItems":2,"items":{"type":"object","required":["source","at","value"],"properties":{"source":{"type":"string","enum":["search","developer","product","employment","social","commerce"]},"at":{"type":"string","format":"date-time"},"value":{"type":"number","description":"Unitless. Momentum is computed from relative change, so raw counts are fine."},"weight":{"type":"number","minimum":0,"maximum":1,"description":"Overrides the default weight for this source."}}}}}}}}},"example":{"topics":[{"topic":"mcp servers","points":[{"source":"search","at":"2026-06-01T00:00:00Z","value":1200},{"source":"search","at":"2026-07-01T00:00:00Z","value":2100},{"source":"search","at":"2026-08-01T00:00:00Z","value":4800},{"source":"developer","at":"2026-06-01T00:00:00Z","value":340},{"source":"developer","at":"2026-07-01T00:00:00Z","value":910},{"source":"developer","at":"2026-08-01T00:00:00Z","value":2400},{"source":"employment","at":"2026-06-01T00:00:00Z","value":12},{"source":"employment","at":"2026-08-01T00:00:00Z","value":47}]}]}}}}}},"/v1/weights":{"get":{"summary":"The default source weights, and why they differ","operationId":"getV1Weights","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/checkout":{"post":{"summary":"Start a hosted Square checkout for a paid tier","description":"Returns a Square-hosted payment URL for the requested tier. No API key is needed — a prospect has not got one yet. The email Square collects becomes the account, so a key issued later for the same address inherits the paid quota. Enterprise returns a contact route rather than a payment link.","operationId":"postV1Checkout","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"checkoutUrl":{"type":"string","format":"uri","description":"Square-hosted payment page. Redirect the buyer here."},"tier":{"type":"string"},"sku":{"type":"string"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tier"],"properties":{"tier":{"type":"string","description":"developer | growth | scale | enterprise"}}},"example":{"tier":"developer"}}}}}},"/api/billing/webhook":{"post":{"summary":"Square billing events, forwarded by the shared hub","description":"Machine-to-machine only. The shared Square billing hub POSTs signed events here; the signature is verified over the exact request bytes. Not callable by API consumers — documented so the integration is auditable.","operationId":"postApiBillingWebhook","security":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}}}},"/v1/keys":{"post":{"summary":"Create a free sandbox API key","description":"Issues a product-scoped key with a free monthly quota, no card required. The key works only on this API. Keep the email — a paid plan bought with the same address raises this key's quota rather than issuing a new one. The key is shown once and is not recoverable.","operationId":"postV1Keys","security":[],"responses":{"201":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"apiKey":{"type":"string","description":"Shown once. Only a peppered hash is stored, so it cannot be recovered later."},"accountId":{"type":"string"},"product":{"type":"string","description":"The key works only on this API."},"quotaPerPeriod":{"type":"integer"},"plan":{"type":"string"},"warning":{"type":"string"},"usage":{"type":"string"},"requestId":{"type":"string"}}}}}},"400":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"example":{"email":"you@example.com"}}}}}},"/v1/usage":{"get":{"summary":"Your consumption and remaining allowance for this period","description":"Reports what this API key has consumed in the current quota period, the ceiling being enforced against it, and when that ceiling resets. Free, and it consumes no quota, so it still answers once you are getting 429 quota_exceeded — which is the point: compare \"used\" against \"ceiling\" and read \"period.resetsAt\" to tell a period you have simply exhausted from a plan that is no longer active, which shows as a \"status\" of canceled and a ceiling of 0. \"used\" counts billing units (see \"unit\"), not HTTP requests: a single request can consume several. All money is in integer minor units (cents), never a decimal. The period reported here is the quota period — the calendar month in UTC, resetting at 00:00 UTC on the 1st. Your invoice window is anchored to the date you subscribed and is a different window; the amounts here are what this period’s usage has accrued so far, not a bill.","operationId":"getV1Usage","security":[{"apiKey":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"product":{"type":"string","description":"The API this key is bound to. A key works only on one."},"tier":{"type":["string","null"],"description":"The plan tier you are billed on, e.g. \"developer\". Null on a free sandbox key with no paid plan."},"status":{"type":"string","enum":["sandbox","active","past_due","canceled"],"description":"State of your plan. \"sandbox\" means no paid plan; \"canceled\" explains a ceiling of 0."},"unit":{"type":"string","description":"The billing unit this API meters. \"used\", \"included\", \"ceiling\" and \"remaining\" are all counted in these, not in requests."},"period":{"type":"object","description":"The quota period: the calendar month in UTC.","properties":{"start":{"type":"string","format":"date-time","description":"00:00 UTC on the 1st of the current month."},"resetsAt":{"type":"string","format":"date-time","description":"When \"used\" returns to 0 — 00:00 UTC on the 1st of next month."}}},"included":{"type":"integer","description":"Units your flat monthly fee already covers, before overage pricing applies. 0 without a paid plan."},"used":{"type":"integer","description":"Units consumed so far this period."},"ceiling":{"type":["integer","null"],"description":"The hard limit enforced against this key: requests are refused beyond it. Null means unlimited."},"remaining":{"type":["integer","null"],"description":"ceiling minus used, never below 0. Null when the ceiling is unlimited."},"overageSoFarMinor":{"type":"integer","description":"What this period’s usage beyond \"included\" has accrued, in minor units, before your spend cap is applied. 0 without a paid plan."},"spendCapMinor":{"type":"integer","description":"The most the overage part of one period can ever cost you, in minor units. Your flat base fee is separate and is never capped away."},"requestId":{"type":"string","description":"Quote this when reporting a problem."}}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"}},"x-quota-cost":0}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Your API key. Create one free at POST /v1/keys. Also accepted as the x-api-key header."}},"responses":{"Error":{"description":"Deterministic error. Branch on `code`; quote `requestId` when reporting a problem.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message","requestId"],"properties":{"code":{"type":"string","enum":["invalid_api_key","missing_api_key","quota_exceeded","rate_limited","invalid_request","not_found","method_not_allowed","payload_too_large","conflict","internal_error"]},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true},"requestId":{"type":"string","format":"uuid"}}}}}}}}}}}