A macro odds dashboard needs the topics list and one event read per tile. Narwhal gives market odds on CPI, payrolls, unemployment, recession and central bank decisions in four named shapes: scalar, binary, timing and extreme.
List the topics that feed the dashboard
Each tile on the dashboard is one topic. The topics endpoint lists them and takes an optional group. The macro groups are central_banks, us_prices, us_jobs, us_activity, global_activity and global_prices. This request asks for us_jobs and keeps the first two topics.
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/predictions/topics?group=us_jobs"{
"topics": [
{
"topic_id": "usa-payrolls",
"group": "us_jobs",
"title": "US nonfarm payrolls change",
"shape": "scalar",
"unit": "jobs",
"unit_key": "job",
"venues": [
"kalshi"
],
"resolution_kind": "release",
"calendar": {
"country": "USA",
"family": "labour"
},
"next_event_id": "usa-payrolls-2026-10"
},
{
"topic_id": "usa-unemployment",
"group": "us_jobs",
"title": "US unemployment rate",
"shape": "scalar",
"unit": "percent",
"unit_key": "percent",
"venues": [
"kalshi",
"polymarket"
],
"resolution_kind": "release",
"calendar": {
"country": "USA",
"family": "labour"
},
"next_event_id": "usa-unemployment-2026-10"
}
]
}shape tells you which endpoint to call: scalar, binary, timing or extreme. unit gives the unit of the answer, venues lists the markets behind it, and resolution_kind says how the topic settles. next_event_id is the event to show now. Some topics, such as deu-gdp-yoy, have no next_event_id, and the script skips them.
Read one event: the recession odds
A binary event asks a yes or no question. This request reads the recession market. The response leaves out the venue rules text.
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/predictions/binary/usa-recession-2026"{
"event_id": "usa-recession-2026",
"topic_id": "usa-recession-2026",
"question": "US recession by end of 2026?",
"status": "trading",
"closes_at": "2026-12-31T00:00:00Z",
"resolves_at": "2026-12-31T00:00:00Z",
"as_of": "2026-11-03T02:21:00Z",
"stale": false,
"resolution": "Two consecutive negative quarters of US real GDP from Q2 2025 to Q4 2026, or an NBER recession call for 2025 or 2026",
"venues": [
{
"venue": "kalshi",
"url": "https://kalshi.com/markets/kxrecssnber/kxrecssnber-26",
"rules_url": "https://assets.kalshi.com/contract_terms/NBERRECSS.pdf",
"settlement_source": "Bureau of Economic Analysis",
"excluded_reason": "rules",
"match": {
"differences": [
"rules"
],
"note": "Kalshi resolves on two negative GDP quarters only; Polymarket also counts an NBER recession call"
},
"volume_24h_usd": "123.64",
"open_interest_usd": "17994.22"
},
{
"venue": "polymarket",
"url": "https://polymarket.com/event/us-recession-by-end-of-2026",
"settlement_source": "Bureau of Economic Analysis; NBER",
"weight": "1.0000",
"volume_24h_usd": "2090.24"
}
],
"probability": "0.0550",
"quotes": [
{
"venue": "kalshi",
"probability": "0.0250",
"market_ids": [
"KXRECSSNBER-26"
]
},
{
"venue": "polymarket",
"probability": "0.0550",
"market_ids": [
"100379208559626151022751801118534484742123694725746262280150222742563282755057"
]
}
]
}probability is the market odds of yes, here 0.0550, and quotes lists the price from each venue. The Kalshi venue entry carries excluded_reason: rules and a match note that its rules count two negative GDP quarters only, while Polymarket also counts a recession call by the research bureau. Kalshi's 0.0250 stays in quotes for reference, and the headline probability equals the Polymarket quote. as_of is the quote time and stale is false while the quotes are live.
The four shapes the API names
| Shape | Question it answers | Fields to read | Example event |
|---|---|---|---|
scalar | Which value or range will a release or decision land in | outcomes, each with key, label, lower, upper, probability | usa-cpi-yoy-2026-11 |
binary | Will it happen | probability, quotes | usa-recession-2026 |
timing | By when will it happen | steps, each with by, label, probability | usa-fed-next-cut-to-2028-12-31 |
extreme | How high or low will a series get | up and down lists of level and probability, plus window | usa-unemployment-peak-2026 |
Scalar events cover the releases that a macro dashboard shows: the 12-month CPI change, the unemployment rate, nonfarm payrolls and the rate decisions of central banks. The resolves_with field of a CPI or unemployment event points to the Narwhal series that settles it, for example /v1/economics/USA/cpi. A tile can therefore link from the odds to the released figure once the event resolves.
A scalar event also names its edges. In an outcome, lower and upper bound the range, and a null edge means the range is open on that side, as with a le_3.0 row for 3.0% or lower. The probability values across the rows add up to 1 in the CPI, unemployment and Federal Reserve events read for this page, so a bar chart of outcomes shows the full distribution.
Build the dashboard rows
The script loops over three groups, reads each topic's next event with the endpoint its shape names, and prints one headline per tile. A scalar shows its likeliest outcome, a binary its yes odds, a timing the first step, and an extreme the first level. A tile without a usable quote says so.
import json
import os
import urllib.request
def get(path):
req = urllib.request.Request(
"https://api.narwhalapi.com" + path,
headers={"Authorization": "Bearer " + os.environ["NARWHAL_API_KEY"]},
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
def headline(shape, e):
if shape == "binary":
if "probability" not in e:
return "no usable quote"
return f"yes {float(e['probability']):.1%}"
if shape == "scalar":
priced = [o for o in e["outcomes"] if "probability" in o]
if not priced:
return "no usable quote"
top = max(priced, key=lambda o: float(o["probability"]))
return f"{top['label']} {float(top['probability']):.1%}"
if shape == "timing":
step = e["steps"][0]
if "probability" not in step:
return "no usable quote"
return f"by {step['label']} {float(step['probability']):.1%}"
if shape == "extreme":
if not e["up"] or "probability" not in e["up"][0]:
return "no usable quote"
step = e["up"][0]
return f"reach {step['level']} {float(step['probability']):.1%}"
for group in ("us_prices", "us_jobs", "us_activity"):
for topic in get("/v1/predictions/topics?group=" + group)["topics"]:
if not topic.get("next_event_id") or topic["shape"] == "scalar" and topic["unit"] == "count":
continue
e = get(f"/v1/predictions/{topic['shape']}/{topic['next_event_id']}")
print(group, e["event_id"], topic["shape"], headline(topic["shape"], e),
"stale" if e["stale"] else "live")
| Event | Shape | Headline |
|---|---|---|
usa-cpi-yoy-2026-11 | scalar | 3.9%, 20.0% |
usa-unemployment-2026-10 | scalar | 4.2%, 37.1% |
usa-unemployment-peak-2026 | extreme | reach 5.0, 3.1% |
usa-recession-2026 | binary | yes 5.5% |
usa-gdp-2026-q4 | scalar | no usable quote |
These lines are the output of one run. The odds move with the market, so read as_of on every tile and refresh the page from the API.
What does a tile with no usable quote mean?
The event response has stale: true, and the probability fields are absent. For emu-ecb-cut-2026 the binary response has an empty quotes list, and its Polymarket venue entry carries excluded_reason: thin. Show such a tile as unavailable rather than as zero, because the response gives no probability to display.
Which topics hold CPI, payrolls and unemployment?
usa-cpi-yoy, usa-cpi-mom, usa-core-cpi-yoy and usa-core-cpi-mom sit in us_prices. usa-payrolls and usa-unemployment sit in us_jobs. usa-recession-2026 sits in us_activity, and the rate decisions sit in central_banks. Query the group, then read each topic's next_event_id.
How do I list upcoming events instead of one per topic?
Call /v1/predictions/events with topic and status=trading. For usa-cpi-yoy it returns the November 2026 event usa-cpi-yoy-2026-11, which resolves at 13:30 UTC on 10 December 2026, and the December 2026 event usa-cpi-yoy-2026-12. Each row has the question, closes_at, resolves_at and shape. Use it for a calendar view of releases that already have a market, and open each event with its shape endpoint.
The same list accepts shape, from, to, limit and cursor, and status=settled returns events that have already resolved.
How do I show a rate decision tile?
Use the central_banks group, or call the policy path for one bank to get cut, hold and hike odds for every priced meeting in one response. The guide to reading Fed rate odds shows the Federal Reserve case.
Published .