Demand signals report
GET https://api.onekey.dev/v1/reports/demand-signals?topic=... searches a topic on community platforms and counts how many of the returned posts look like someone wanting to buy, asking a question, or complaining, with the top examples of each. See Reports for the shape and rules every report shares.
This report is AI-dependent and not enabled yet (see the note above): the counts are judgments. The searches, the volume per platform and the coverage are not; they are the same deterministic legs as any multi-platform search.
curl "https://api.onekey.dev/v1/reports/demand-signals?topic=mechanical%20keyboard&platforms=hackernews,devto,reddit&top=2" \
-H "x-api-key: YOUR_API_KEY"Inputs#
| Parameter | Values | What it does |
|---|---|---|
topic | text, required, up to 200 characters | The topic to search for. |
platforms | comma list | Search exactly these. Default: the pool, which is Bluesky, DEV Community, Hacker News, Lemmy, Reddit, Stack Exchange. |
max_platforms | 1 to 12, default 6 | Cap on the platform list. |
min_p | 0 to 1, default 0.5 | A row counts for a signal when its probability is at or above this. |
top | 1 to 50, default 5 | How many examples per signal. |
relevance, threshold | score or filter; 0 to 1 | Also judge each row against the topic. |
themes | true | Add stats.top_themes. |
strict | true (default) or false | What happens when nothing could be judged. |
dry_run | true | See Dry run. |
The report always labels purchase_intent, question and complaint, so include and label are refused with INVALID_PARAMS. Their price is metered per judged row on top of the legs.
The default pool is small because it needs searches that accept labels. You may name other platforms in platforms. Their rows are then counted but not judged (labels.unsupported lists them).
The response#
On top of the shared shape:
| Field | Meaning |
|---|---|
stats.rows | Rows over the legs that answered. |
stats.volume_per_platform | Rows per platform, null for a platform whose leg failed. |
stats.purchase_intent, stats.question, stats.complaint | For each: preset, threshold (= min_p), rows, judged, unjudged, count (rows at or above min_p) and share_of_judged (count / judged). |
stats.top_themes | The largest clusters when themes=true, else null. |
examples.<signal> | The top rows by probability for each signal, with platform, p and the item. |
labels | The labels block: presets, labelled, extra_credits and methodology_version. |
share_of_judged divides by the rows that were judged, not by all rows. A row that could not be judged is in unjudged and never counts for or against a signal.
A recorded call#
Recorded on the dev API with the pre-release test provider, shortened. Reddit failed and is shown as such:
{
"success": true,
"data": {
"report": "demand_signals",
"methodology_version": "demand_signals/1",
"inputs": {
"topic": "mechanical keyboard", "platforms": ["hackernews", "devto", "reddit"], "selection": "explicit",
"labels": ["purchase_intent", "question", "complaint"],
"subqueries": { "hackernews": "mechanical keyboard", "devto": "mechanical", "reddit": "mechanical keyboard" },
"adapted": { "devto": "first keyword only" }, "skipped": {},
"min_p": 0.5, "top": 2, "themes": false, "relevance": null
},
"coverage": {
"answered": ["hackernews", "devto"],
"failed": [ { "platform": "reddit", "endpoint": "search_posts", "status": 502, "type": "UPSTREAM_BLOCKED" } ],
"missing_fields": {}
},
"stats": {
"rows": 20,
"volume_per_platform": { "hackernews": 10, "devto": 10, "reddit": null },
"purchase_intent": { "preset": "purchase_intent", "threshold": 0.5, "rows": 20, "judged": 20, "unjudged": 0, "count": 1, "share_of_judged": 0.05 },
"question": { "preset": "question", "threshold": 0.5, "rows": 20, "judged": 20, "unjudged": 0, "count": 3, "share_of_judged": 0.15 },
"complaint": { "preset": "complaint", "threshold": 0.5, "rows": 20, "judged": 20, "unjudged": 0, "count": 0, "share_of_judged": 0.0 },
"top_themes": null
},
"examples": {
"purchase_intent": [ { "platform": "devto", "p": 0.55, "item": { "content": { "id": "4561995", "title": "Product Sliding Guide Rail Design: ..." } } } ],
"question": [ { "platform": "devto", "p": 0.92, "item": { "content": { "id": "4648836", "title": "How Do I Choose the Right Mechanical Contractor for a Project in KSA?" } } } ]
},
"labels": { "status": "complete", "presets": ["complaint", "purchase_intent", "question"], "rows": 20, "labelled": 20, "extra_credits": 19.695, "methodology_version": "fake/1" },
"ai_methodology_version": "fake/1",
"credits": { "legs": 10.0, "ai": 19.695, "total": 29.695 }
},
"metadata": { "recipe": "demand_signals", "credits_charged": 29.695, "extra_credits": 19.695 }
}Read this the way the report asks you to. The DEV Community search is tag-shaped, so "mechanical keyboard" became the tag mechanical (visible in inputs.subqueries and adapted), and the examples are about mechanical contractors and guide rails, not keyboards. The test provider's purchase_intent is keyword matching on words such as "purchase", "buy" and "price". These counts prove that the plumbing works. They are not findings about keyboards.
Costs#
Measured on the pre-release test provider (synthetic usage, so it shows how the charge scales, not what production judgments will cost): 10 credits for the legs (the failed Reddit leg was refunded), 19.695 for judging 20 rows with 3 presets (0.98 per row), 29.695 in all. See AI pricing.
When AI is not enabled#
With the default strict=true the call is a free AI_UNAVAILABLE before any fetch. With strict=false it answers at the legs' price only, with every count reporting judged: 0 and empty examples.
Limits#
- Every count is an AI judgment. On the pre-release test provider they are keyword heuristics; nothing here has been run against a production model.
- The default pool is small (6 platforms today) because it needs searches that accept labels.
- Tag-style searches receive one keyword, which can change what a topic means.
inputs.adaptedshows it. - Counts cover the first page of each platform's search, not everything ever posted about the topic.
- Reports are JSON only. A
Accept: text/event-streamheader is ignored, and a report is not yet a target for a monitor.