DemandGraph API
Fuse search, developer, product, employment and commerce signals into demand momentum, acceleration, commercial-intent and durability scores.
Quickstart
Two calls: get a key, then use it. No account, no dashboard, no card.
1. Get a free key
curl -X POST https://web-production-37b02.up.railway.app/v1/keys \
-H 'content-type: application/json' \
-d '{"email":"you@example.com"}'
Returned once — only a peppered hash is stored, so it cannot be recovered later. Free tier: 50 scored topics/month.
2. Call the API
curl -X POST https://web-production-37b02.up.railway.app/v1/scores \
-H "Authorization: Bearer $KEY" \
-H 'content-type: application/json' \
-d '{"topics":[{"topic":"mcp servers","points":[
{"source":"search","at":"2026-06-01T00:00:00Z","value":1200},
{"source":"search","at":"2026-08-01T00:00:00Z","value":4800}]}]}'
Or skip the terminal: every endpoint below has a Try it panel that sends the real request from this page. Start at the playground.
Authentication
Authorization: Bearer sp_live_…
x-api-key is also accepted. Keys are scoped to this API — a key issued for another
product returns invalid_api_key, identical to an unknown key, so the API will not
confirm a key is valid elsewhere. Never put a key in browser JavaScript; authenticated
endpoints deliberately do not support CORS.
Playground
Every endpoint below carries a Try it panel, prefilled with a working request. Edit it,
press Send, and read what this API actually answered — status, timing, requestId and body,
unedited. Requests go straight from your browser to https://web-production-37b02.up.railway.app; nothing is proxied
through this page and no request is recorded here.
What happens to this key. It is a live credential, and it is treated as one. It is held
in this page's memory for as long as the tab is open and nowhere else — not
localStorage, not sessionStorage, not a cookie, not the URL, not a
query string. It is never logged, and never sent to the server that serves this page. The only
place it goes is the Authorization: Bearer header of requests you send to
https://web-production-37b02.up.railway.app. Reloading or closing the tab discards it; Clear discards it now.
Copied curl commands carry $KEY, never the key itself. On a shared machine,
clear it when you are done.
What a call costs. Every panel says so above its Send button, taken from the same contract as the rest of this page. No key required is a public endpoint — no key, no quota, no charge. Free — no quota needs your key but reserves nothing. Billable runs against your real account and reserves the stated quota from your plan before it runs, exactly as it would from your own code. Quota: no fixed cost also runs against your real account, but the contract states no fixed amount: endpoints whose work scales with the payload reserve an amount derived from what you send, so try a small request first.
No key yet? Open the Try it panel on POST /v1/keys, put in your email and
press Send — the response comes back with a button that loads the new key straight into this
field.
Endpoints
/v1/keysCreate a free sandbox API key
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.
Try itPOST /v1/keysNo key required
Public endpoint. No API key, no quota, nothing billed.
Request body
| Field | Type | Notes | |
|---|---|---|---|
email | string | required | Format: email. |
Example request
{
"email": "you@example.com"
}
curl
curl -X POST https://web-production-37b02.up.railway.app/v1/keys \
-H 'content-type: application/json' \
-d '{"email":"you@example.com"}'
Response
| Field | Type | Notes |
|---|---|---|
apiKey | string | Shown once. Only a peppered hash is stored, so it cannot be recovered later. |
accountId | string | |
product | string | The key works only on this API. |
quotaPerPeriod | integer | |
plan | string | |
warning | string | |
usage | string | |
requestId | string |
/v1/scoresScore and rank topics by demand momentum
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.
Try itPOST /v1/scoresQuota: no fixed cost
Runs against your real account. The contract gives this endpoint no fixed quota cost: nothing is reserved before it runs, and endpoints whose work scales with the payload reserve an amount derived from the body instead — so a large request here can cost more than one unit. Send a small one first.
Using the API key from the playground key field — no key set yet. It is sent only in the Authorization: Bearer header, only to https://web-production-37b02.up.railway.app.
Your browser will not send an Authorization header to this API from this page: authenticated endpoints do not enable cross-origin access, deliberately, so that a key can never be read out of a web app’s front-end. Press Show as curl and run the request from a terminal instead — your edits are carried into it.
Request body
| Field | Type | Notes | |
|---|---|---|---|
topics | object[] | required | At least 1. At most 200. |
weights | object | optional | 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 request
{
"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
}
]
}
]
}
curl
curl -X POST https://web-production-37b02.up.railway.app/v1/scores \
-H "Authorization: Bearer $KEY" \
-H 'content-type: application/json' \
-d '{"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}]}]}'
Response
| Field | Type | Notes |
|---|---|---|
count | integer | |
scores | object[] |
/v1/demo/scorePublic demo — scores one topic without a key
Runs the real scoring engine. Capped at 3 topics and 60 observations. Nothing is stored, nothing is metered.
Try itPOST /v1/demo/scoreNo key required
Public endpoint. No API key, no quota, nothing billed.
Request body
| Field | Type | Notes | |
|---|---|---|---|
topics | object[] | required | At most 3. |
Example request
{
"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
}
]
}
]
}
curl
curl -X POST https://web-production-37b02.up.railway.app/v1/demo/score \
-H 'content-type: application/json' \
-d '{"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}]}]}'
Response
/v1/weightsThe default source weights, and why they differ
Try itGET /v1/weightsNo key required
Public endpoint. No API key, no quota, nothing billed.
curl
curl -X GET https://web-production-37b02.up.railway.app/v1/weights
Response
/v1/checkoutStart a hosted Square checkout for a paid tier
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.
Try itPOST /v1/checkoutNo key required
Public endpoint. No API key, no quota, nothing billed.
Request body
| Field | Type | Notes | |
|---|---|---|---|
tier | string | required | developer | growth | scale | enterprise |
Example request
{
"tier": "developer"
}
curl
curl -X POST https://web-production-37b02.up.railway.app/v1/checkout \
-H 'content-type: application/json' \
-d '{"tier":"developer"}'
Response
| Field | Type | Notes |
|---|---|---|
checkoutUrl | string | Square-hosted payment page. Redirect the buyer here. Format: uri. |
tier | string | |
sku | string | |
requestId | string |
/api/billing/webhookSquare billing events, forwarded by the shared hub
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.
Try itPOST /api/billing/webhookNo key required
Public endpoint. No API key, no quota, nothing billed.
The contract declares no request schema for this endpoint, so there is nothing to prefill. See the description above for what it accepts.
curl
curl -X POST https://web-production-37b02.up.railway.app/api/billing/webhook
Response
/v1/usageYour consumption and remaining allowance for this period
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.
Try itGET /v1/usageFree — no quota
Needs your API key. The contract gives this endpoint no quota cost and it takes no request body, so nothing is reserved and nothing is billed.
Using the API key from the playground key field — no key set yet. It is sent only in the Authorization: Bearer header, only to https://web-production-37b02.up.railway.app.
Your browser will not send an Authorization header to this API from this page: authenticated endpoints do not enable cross-origin access, deliberately, so that a key can never be read out of a web app’s front-end. Press Show as curl and run the request from a terminal instead — your edits are carried into it.
curl
curl -X GET https://web-production-37b02.up.railway.app/v1/usage \
-H "Authorization: Bearer $KEY"
Response
| Field | Type | Notes |
|---|---|---|
product | string | The API this key is bound to. A key works only on one. |
tier | string | null | The plan tier you are billed on, e.g. "developer". Null on a free sandbox key with no paid plan. |
status | "sandbox" | "active" | "past_due" | "canceled" | State of your plan. "sandbox" means no paid plan; "canceled" explains a ceiling of 0. |
unit | string | The billing unit this API meters. "used", "included", "ceiling" and "remaining" are all counted in these, not in requests. |
period | object | The quota period: the calendar month in UTC. |
included | integer | Units your flat monthly fee already covers, before overage pricing applies. 0 without a paid plan. |
used | integer | Units consumed so far this period. |
ceiling | integer | null | The hard limit enforced against this key: requests are refused beyond it. Null means unlimited. |
remaining | integer | null | ceiling minus used, never below 0. Null when the ceiling is unlimited. |
overageSoFarMinor | integer | What this period’s usage beyond "included" has accrued, in minor units, before your spend cap is applied. 0 without a paid plan. |
spendCapMinor | integer | 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 | string | Quote this when reporting a problem. |
/Service index — endpoints, auth and error format
Try itGET /No key required
Public endpoint. No API key, no quota, nothing billed.
curl
curl -X GET https://web-production-37b02.up.railway.app/
Response
/healthLiveness and deployed version
Try itGET /healthNo key required
Public endpoint. No API key, no quota, nothing billed.
curl
curl -X GET https://web-production-37b02.up.railway.app/health
Response
Errors
Every failure returns the same shape. Branch on code — it is a stable enum.
message is for humans and may change.
{
"error": {
"code": "quota_exceeded",
"message": "Quota exceeded for this billing period.",
"requestId": "0f3c8b12-…"
}
}
| Code | HTTP | Meaning |
|---|---|---|
missing_api_key | 401 | No Authorization or x-api-key header |
invalid_api_key | 401 | Unknown, revoked, or issued for a different API |
quota_exceeded | 429 | Allowance used for this period |
rate_limited | 429 | Too many requests |
invalid_request | 400 | Validation failed — details.path names the exact field |
not_found | 404 | No such route or resource |
internal_error | 500 | Our fault, not yours |
requestId appears on every response — success or failure — and in the
x-request-id header. Quote it and we can find the exact request. The playground
shows it above each response.
SDKs
Single files, zero dependencies — no requests, no axios — so they drop
into any environment without a dependency negotiation.
Python
from demandgraph import DemandGraph
client = DemandGraph() # reads DEMANDGRAPH_API_KEY
res = client.score([
{"topic": "mcp servers", "points": [
{"source": "search", "at": "2026-06-01T00:00:00Z", "value": 1200},
{"source": "search", "at": "2026-08-01T00:00:00Z", "value": 4800},
{"source": "employment", "at": "2026-06-01T00:00:00Z", "value": 12},
{"source": "employment", "at": "2026-08-01T00:00:00Z", "value": 47},
]},
])
for s in res["scores"]:
# Discard thin reads rather than acting on them.
if s["confidence"] < 50:
continue
print(s["topic"], s["trend"], s["momentum"], "intent", s["commercialIntent"])
TypeScript
import { DemandGraph } from './demandgraph.js'
const client = new DemandGraph() // reads DEMANDGRAPH_API_KEY
const res = await client.score([
{ topic: 'mcp servers', points: [
{ source: 'search', at: '2026-06-01T00:00:00Z', value: 1200 },
{ source: 'search', at: '2026-08-01T00:00:00Z', value: 4800 },
{ source: 'employment', at: '2026-06-01T00:00:00Z', value: 12 },
{ source: 'employment', at: '2026-08-01T00:00:00Z', value: 47 },
] },
])
for (const s of res.scores) {
if (s.confidence < 50) continue // thin evidence, not a signal
console.log(s.topic, s.trend, s.momentum, 'intent', s.commercialIntent)
}
Both live in the repo under sdk/, alongside a Postman collection.
The TypeScript client runs on Node 18+, Deno, Bun and Workers.