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.
https://trade-news.gptsci-supp.workers.dev
Authorization: Bearer <your-key>
application/json
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:
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
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:
predicted_price— required, positive number. Your BTC forecast for the target time.target_time— optional, epoch ms (number) or ISO-8601 string. When the forecast is supposed to hold. Default isreference_time + 1h(the legacy "1 hour ahead" cadence), but you can submit any future time — the grader will settle the prediction once that timestamp passes. Must be strictly afterreference_time.window_id— optional integer. Pin the prediction to a specific window. Omit it to predict against the latest window automatically — usually what live collectors want.value— optional string, default"point". Names the series this prediction belongs to within the model. Use it to publish multiple values per window from one key — common choices:point,max,min,median,q05,q95. Allowed: lowercase a–z, 0–9,_and-, up to 50 chars. The leaderboard groups by (model, value), so each named series gets its own accuracy row.target_symbol— optional, defaults to"BTC".reference_price— optional override; defaults to the window'sbtc_spot_price.reference_time— optional, epoch ms or ISO-8601. Defaults to the window'swindow_end_ms. The "starting point" the direction-correctness metric is computed against (sign(predicted − reference) === sign(actual − reference)).
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
201 Created— submission accepted.200 OK— download succeeded.400 Bad Request— body or query failed schema validation; the response includes aissuesarray describing each failure.401 Unauthorized— missing or invalid bearer token.403 Forbidden— token is valid but doesn't carry the required scope (e.g. read-only key trying to POST).404 Not Found— requested id doesn't exist.409 Conflict— predicting against a non-existent latest window.429 Too Many Requests— anonymous read quota exceeded; checkRetry-Afteror use a bearer key to bypass the limit.500 Internal Error— see Worker logs / try again.
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")'