Trade News Hub API reference

API reference

Programmatic access to hourly Bitcoin news windows, BTC price ticks, model predictions, and the prediction leaderboard. Read endpoints (GET) are publicly accessible up to a per-IP rate limit; submit endpoints (POST) require a bearer key. Pass a key on read endpoints and the rate limit lifts entirely.

Base URL https://trade-news.gptsci-supp.workers.dev
Auth header Authorization: Bearer <your-key>
Format application/json
Anon rate limit 60 req / min / IP (lifted with key)

1 · Authentication & rate limits

Read endpoints (GET /api/news, GET /api/news/:id, GET /api/prices, GET /api/predictions, GET /api/leaderboard) work anonymously up to 60 requests per minute per IP. Beyond that limit the server replies with 429 Too Many Requests and a Retry-After header.

Every response carries quota headers so clients can throttle themselves:

X-RateLimit-Limit:     60
X-RateLimit-Remaining: 58
X-RateLimit-Reset:     1780668060

Submitting data (POST /api/news, POST /api/predictions) always requires a write or admin bearer token. Passing any valid bearer token on a read endpoint removes the rate limit, which is recommended for collectors and automation.

Tokens come in three scopes:

ScopePOST /api/newsPOST /api/predictionsGET (download)Admin ops
read nonoyesno
predict noyesyesno
write yesyesyesno
admin yesyesyesyes

A predict key is the least-privilege option for a forecasting model: it can submit predictions and read public data (downloads bypass the anonymous rate limit), but cannot post news windows or touch admin endpoints.

A key is shown in plaintext exactly once at mint time and only its SHA-256 hash is stored. If you lose a key, mint a new one and revoke the old; there is no recovery path.

Get a key

Ask the hub operator. Keys are minted by an admin via the bootstrap admin endpoint. The label you choose becomes the model attribution on every prediction you submit with that key — pick it carefully (it's how your model shows up on the leaderboard).

The repo ships a helper script that wraps the curl call, validates the scope, prompts for the bootstrap key without echoing it, and prints the new key in a save-it-now panel:

# From the repo root
./mint-key.sh --label live_v1 --scope predict

# Or run interactively — the script prompts for anything missing:
./mint-key.sh

The raw curl below does the same thing if you prefer:

# A least-privilege prediction-only key (recommended for forecasters):
curl -X POST "$BASE/api/admin/keys" \
  -H "Authorization: Bearer $ADMIN_BOOTSTRAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label":"live_v1","scopes":"predict"}'

# 201
# {
#   "id": 7,
#   "label": "live_v1",
#   "scopes": "predict",
#   "api_key": "tn_live_…",
#   "note": "Store this key now — it cannot be retrieved again."
# }

Once you have a key, paste it into the topbar "key" pill on the dashboard to use it from this browser, or set TRADE_NEWS_API_KEY in your shell for the curl examples below.

2 · Submit data

Submission requires a write or admin-scoped key. There are two write endpoints — one per hourly news window, and one per model prediction.

POST /api/news

Send one hourly bundle. The hub derives window identity from window_start / window_end, stores the parent row plus each item, and returns the assigned id. Not idempotent — POST exactly once per hour.

curl -X POST "$BASE/api/news" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  -H "Content-Type: application/json" \
  --data @hourly-record.json

# 201
# { "window_id": 42, "item_count": 5, "status": "stored" }
Body schema (excerpt)
{
  "window_start": "2026-06-01T13:00Z",
  "window_end":   "2026-06-01T14:00Z",
  "collected_at": "2026-06-01T14:01Z",
  "quiet_period": false,
  "market_context": {
    "btc_spot_price": 74250, "btc_price_change_1h_pct": 0.42,
    "btc_volume_1h": 720, "dxy": 105.1, "funding_rate": 0.0002
  },
  "aggregate_features": {
    "net_sentiment": 0.62, "total_impact": 0.41,
    "item_count": 4, "max_single_impact": 0.18,
    "bullish_count": 3, "bearish_count": 1,
    "regulatory_flag": 1, "event_shock_flag": 0,
    "category_distribution": {"regulatory": 1, "institutional_flows": 1}
  },
  "items": [
    {
      "headline": "...", "source": "Reuters", "url": "...",
      "timestamp": "2026-06-01T13:08Z", "summary": "...",
      "category": "regulatory", "scope": "bitcoin_specific",
      "impact_score": 5, "sentiment_score": 5, "sentiment_confidence": 5,
      "source_credibility": 5, "novelty_score": 5, "recency_score": 5,
      "mention_volume": 6, "quantitative_value": null, "quantitative_unit": null,
      "weighted_impact": 1.0
    }
  ]
}

POST /api/predictions

Forecast BTC price one hour after the window's end. The hub fills reference_price / reference_time from the window if omitted; the grader cron settles each pending prediction once the target time passes.

Model attribution. There is no model field in the request body. The prediction's model name is taken from the label on the API key you authenticate with at submit time. To submit under the name live_v1, mint a key with {"label":"live_v1","scopes":"predict"} and post with that key — the response and leaderboard will both say live_v1. A single model can publish multiple named values per window (point/max/min/quantiles) by setting the value field, described below.

Body fields:

The response always echoes the resolved target_time and a submitted_at timestamp (server-recorded epoch ms when the row was inserted) so callers can audit the round-trip.

Point prediction example — let the hub default target_time to reference_time + 1h:

curl -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"predicted_price": 74600}'

# 201
# {
#   "id": 81, "model": "live_v1", "value": "point",
#   "window_id": 42, "reference_price": 74250,
#   "reference_time": 1780668000000,
#   "target_time":    1780671600000,
#   "submitted_at":   1780668003121,
#   "status": "pending"
# }

Explicit target time — forecast 4 hours out using an ISO timestamp:

curl -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "predicted_price": 75100,
    "target_time": "2026-06-01T18:00:00Z"
  }'

Range / multi-value example — same key (live_v1), three named values per window:

# Upper bound
curl -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"predicted_price": 75200, "value": "max"}'

# Lower bound
curl -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"predicted_price": 74100, "value": "min"}'

# Median (also the point estimate)
curl -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"predicted_price": 74650, "value": "median"}'

Filter the predictions listing by value, too:

curl "$BASE/api/predictions?model=live_v1&value=max&status=graded" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY"

Or pin the prediction to a specific window you've inspected:

# Find the window_id first (newest is items[0].id)
WINDOW_ID=$(curl -s "$BASE/api/news?limit=1" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  | jq -r '.items[0].id')

curl -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"window_id\": $WINDOW_ID, \"predicted_price\": 74600}"

POSTs are not idempotent — calling /api/predictions twice with the same payload creates two pending predictions. Submit each forecast exactly once per window per model.

3 · Download data

Download endpoints accept any active bearer token, or work anonymously under the per-IP rate limit. All list endpoints are newest-first and cursor-paginated via before=<id>. The examples below include the Authorization header — it's optional, but omitting it counts the call against your IP quota.

GET /api/news

List recent windows.

curl "$BASE/api/news?limit=20" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY"

# 200
# {
#   "items": [
#     {
#       "id": 42, "window_end": "2026-06-01T14:00Z",
#       "btc_spot_price": 74250, "net_sentiment": 0.62,
#       "total_impact": 0.41, "item_count": 4,
#       "bullish_count": 3, "bearish_count": 1,
#       "regulatory_flag": 1, "event_shock_flag": 0, "quiet_period": 0,
#       "category_distribution": {"regulatory": 1, "institutional_flows": 1},
#       ...
#     }
#   ],
#   "next_cursor": 22
# }

Query params: limit (1–200, default 50), before (id cursor).

GET /api/news/:id

One window plus all of its items.

curl "$BASE/api/news/42" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY"

# 200
# {
#   "id": 42, "window_end": "2026-06-01T14:00Z", ...,
#   "items": [
#     {"headline": "...", "category": "regulatory", "weighted_impact": 1.0, ...}
#   ]
# }

GET /api/prices

BTC tick series for charting. Params: symbol (default BTC), hours (1–168, default 24).

curl "$BASE/api/prices?hours=4" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY"

# 200
# { "symbol": "BTC", "ticks": [{"t": 1780664400000, "price": 74250, "source": "coinbase"}, ...] }

GET /api/predictions

List predictions. Params: limit, before, status (pending|graded|expired), model.

curl "$BASE/api/predictions?status=graded&limit=25" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY"

GET /api/leaderboard

Per-(model, value) accuracy across graded predictions. Each named series gets its own row, so a range predictor that posts max and min under one key has two leaderboard entries.

curl "$BASE/api/leaderboard" \
  -H "Authorization: Bearer $TRADE_NEWS_API_KEY"

# 200
# {
#   "leaderboard": [
#     {"rank": 1, "model": "alpha",   "value": "point",
#      "graded_count": 17, "avg_error_pct": 0.74, "direction_accuracy_pct": 70.5},
#     {"rank": 2, "model": "live_v1", "value": "median",
#      "graded_count": 12, "avg_error_pct": 0.81, "direction_accuracy_pct": 66.7},
#     {"rank": 3, "model": "live_v1", "value": "max",
#      "graded_count": 12, "avg_error_pct": 1.20, "direction_accuracy_pct": 58.3}
#   ]
# }

GET /api/health

The single unauthenticated endpoint. Returns server time, the timestamp of the most recent price tick, and pending-prediction counts — used by the dashboard health pill.

4 · Status codes

5 · End-to-end: submit a live prediction

Full flow to submit a BTC forecast under the model name live_v1 and check that it landed on the leaderboard.

BASE=https://trade-news.gptsci-supp.workers.dev

# 1. One-time: ask the admin to mint a key labelled "live_v1".
#    The label is what shows up as the model name — pick it now.
#    "predict" scope is least-privilege (can submit predictions only).
curl -s -X POST "$BASE/api/admin/keys" \
  -H "Authorization: Bearer $ADMIN_BOOTSTRAP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label":"live_v1","scopes":"predict"}'
# → { "id": 7, "label": "live_v1", "scopes": "predict", "api_key": "tn_live_…" }

export KEY=tn_live_…  # paste the api_key value returned above

# 2. Every hour after a new window posts, submit a prediction.
#    Omit window_id and the hub pins it to the latest window.
curl -s -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"predicted_price": 74600}'
# → { "id": 81, "model": "live_v1", "window_id": 42, "status": "pending", ... }

# 3. (Optional) Range / multi-value publisher: post several named values
#    under the same key by setting "value".
curl -s -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"predicted_price": 75200, "value": "max"}'
curl -s -X POST "$BASE/api/predictions" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"predicted_price": 74100, "value": "min"}'

# 4. After the target time (~1h later) the grader settles each prediction.
#    Filter by (model, value) to inspect just one series:
curl -s "$BASE/api/predictions?model=live_v1&value=max&limit=10" \
  -H "Authorization: Bearer $KEY" \
  | jq '.items[] | {predicted_price, actual_price, error_pct, status}'

# 5. Each named series shows up as its own leaderboard row:
curl -s "$BASE/api/leaderboard" \
  | jq '.leaderboard[] | select(.model == "live_v1")'