A rate-odds display needs three things: a percentage for a cut, no change and a rise at each meeting, the time those prices were set, and wording a customer reads correctly. Narwhal returns the odds per meeting with an as_of time, and this page covers the display rules.
Fetch the path once and show when it was priced
A product that shows rate odds has two problems before any design work. The numbers move, and a screen that was right when it loaded can be wrong an hour later. One request returns every priced meeting for a central bank, plus the time the quotes were taken. This one asks for the Federal Reserve and keeps the first two meetings.
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/predictions/policy-paths/federal-reserve"{
"as_of": "2026-11-03T03:12:00Z",
"meetings": [
{
"event_id": "usa-fed-2026-12",
"p_cut": "0.0096",
"p_hold": "0.2533",
"p_hike": "0.7371",
"expected_change_bps": "18.6",
"cumulative_change_bps": "18.6",
"resolves_at": "2026-12-09T19:00:00Z"
},
{
"event_id": "usa-fed-2027-01",
"p_cut": "0.0280",
"p_hold": "0.7055",
"p_hike": "0.2665",
"expected_change_bps": "6.0",
"cumulative_change_bps": "24.6",
"resolves_at": "2027-01-27T19:00:00Z"
}
]
}p_cut, p_hold and p_hike are decimal strings between 0 and 1 that add up to 1 for each meeting. resolves_at is the decision time. as_of is the time of the quotes, which is the time to show on screen, not the time your server made the request.
Turn the fields into a sentence a customer reads correctly
A figure such as 73.71% reads like a forecast. Four rules keep the screen honest. Round to whole percents. Make the three figures add up to 100. Call them what traders are pricing. Print the time next to them. The script applies the first two rules and builds the line. A plain round would show 27%, 71% and 3% for the 27 January meeting, which adds up to 101, so the script gives the spare point to the largest remainder.
import json, os, urllib.request
from datetime import datetime, timezone
from decimal import Decimal
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 whole_percent(odds):
"""Round to whole percents that add up to 100 (largest remainder)."""
raw = {k: v * 100 for k, v in odds.items()}
out = {k: int(v) for k, v in raw.items()}
for k in sorted(raw, key=lambda k: raw[k] - out[k], reverse=True)[:100 - sum(out.values())]:
out[k] += 1
return out
path = get("/v1/predictions/policy-paths/federal-reserve")
as_of = datetime.fromisoformat(path["as_of"].replace("Z", "+00:00"))
age = int((datetime.now(timezone.utc) - as_of).total_seconds() // 60)
for m in path["meetings"][:3]:
day = datetime.fromisoformat(m["resolves_at"].replace("Z", "+00:00"))
p = whole_percent({k: Decimal(m["p_" + k]) for k in ("cut", "hold", "hike")})
print(f"{day.day} {day:%B}: {p['hike']}% rise, {p['hold']}% no change, {p['cut']}% cut")
print(f"Market prices as of {as_of:%H:%M} UTC, {age} min old")
9 December: 74% rise, 25% no change, 1% cut
27 January: 27% rise, 70% no change, 3% cut
17 March: 50% rise, 44% no change, 6% cut
Market prices as of 03:12 UTC, 0 min old| Field | Wording for a customer |
|---|---|
p_hike | Chance of a rate rise |
p_hold | Chance rates stay the same |
p_cut | Chance of a rate cut |
as_of | Prices as of this time, in the customer's time zone |
expected_change_bps | For a dealing room: the average move the market prices, in basis points |
These are prices on contracts, so write that they are market odds. Avoid wording such as "the Fed will". For a dealing room, expected_change_bps and cumulative_change_bps give the average move per meeting and the running total, in basis points.
Refresh the display on a schedule you choose
Narwhal answers when asked, so the product decides how often to ask. Fetch on your own interval and keep the last good response in a cache that every page view reads. Compare as_of with the clock before drawing. The script prints the age in minutes, and a product can show a note or hide the card when the age passes a limit that the business sets.
The policy path carries only as_of. For a single meeting, GET /v1/predictions/scalar/{event_id} adds status and a stale flag, so a confirmation step can check that a card is still live. Drop a row whose resolves_at is already in the past.
Cache the response so that traffic from users does not reach the API. The how-to on caching responses and handling 429 errors covers the retry rules.
Why do rounded odds sometimes add up to 99 or 101?
Each figure is rounded alone. At the 27 January meeting the unrounded odds are 26.65 for a rise, 70.55 for no change and 2.80 for a cut. Rounding each gives 27, 71 and 3, which is 101. The largest-remainder step in the script adds the missing points to the figures with the biggest remainders and gets 27, 70 and 3.
Can the screen call these a forecast?
The odds are prices of contracts traded on Kalshi and Polymarket, shown as probabilities. Narwhal does not forecast. Word the card as market odds, show the time, and name the source of the contracts. Whether wording on a regulated product needs review is the company's own decision.
Which central banks can the display cover?
Ask GET /v1/predictions/topics?group=central_banks for the decision markets. The list holds a topic for each bank, such as the Federal Reserve, the Reserve Bank of Australia, the Bank of Japan and Bank Indonesia, and a next_event_id for the next meeting. The venues field differs by topic. Bank Indonesia lists only Kalshi, while the Federal Reserve lists Kalshi and Polymarket. The policy path answers for federal-reserve, european-central-bank, bank-of-japan, bank-of-canada, reserve-bank-of-australia, bank-of-england and bank-indonesia.
How many meetings does the path hold?
In the run above the Federal Reserve path held eight meetings, from December 2026 into 2027, and each row has its own resolves_at. The script shows three. A longer chart can plot all rows, and cumulative_change_bps gives the running total of the average moves. The guide on reading Fed rate odds explains each field.
Published .