Overview
Four services on one host, https://news.opearl.ai, for a set of subscribed tickers.
Live
The latest headline about any ticker, scored on demand: score and headline in one JSON answer.
Sub, minute by minute
The whole history of a ticker as a CSV file, one row a minute, updated every minute with a link that returns only the new rows. Plus an hourly breakout file.
Create a ticker
Ask for a ticker that is not served yet: its archive is downloaded and scored, its files built, and it is updated from then on.
Alerts
Every new or revised headline of a ticker, pushed to a Telegram chat or a mailbox, with optional filters.
What is served today
| Ticker | Kind | News rows from | Breakout rows from |
|---|---|---|---|
| QQQ (also as TQQQ) | Index ETF, scored from its constituents | 2016-01-04 | 2016-01-04 |
| SOXX (also as SOXL) | Index ETF, scored from its constituents | 2016-01-04 | 2016-01-04 |
| TSLA | Stock | 2010-06-29 | 2010-09-23 |
| BTC | Crypto | 2021-02-09 | 2015-10-17 |
| EURUSD, GBPUSD, USDJPY | FX pairs | 2021-01 | 2016-01-04 |
| ROKU | Stock (created on request) | 2017-09-28 | 2017-12 |
Start with Telegram — no key needed
Open @opearl_news_bot in Telegram and press Start. Everything below works from the chat.
| Command | What it does |
|---|---|
/sub TSLA | Alerts for TSLA on this chat: every new headline (item 1) and every revised headline (item 2), with the score, the headline and its link. |
/unsub TSLA | Stop them. |
/list | What this chat follows. |
/create AAPL | Build a ticker that is not served yet: the whole archive, the scores, the minute file, the breakout history. One creation runs at a time. The bot messages the chat when the ticker is ready and the chat follows it from then on. A mid-cap takes a few minutes, a large cap up to an hour. |
/status | The creation in progress: headlines downloaded, scored, percentage, time left. |
/help | The commands and your chat id. |
The API
Base URL https://news.opearl.ai. Every route except /v1/ping and the alert confirmation needs your key.
X-API-Key: <your key> # or: Authorization: Bearer <your key>
A key in the URL is refused. Every error is one JSON envelope: {"error": {"code", "message", "retryable"}}.
| Route | Answers |
|---|---|
GET /v1/ping | Public. ok, ready, and the minute job's lag in seconds. |
GET /v1/live/{ticker} | The latest headline of any ticker, scored now: score, headline, published, as_of, age_seconds, stale, cached. ?fields=score drops the headline, ?max_age=<seconds> refuses old news. For an FX pair the answer also carries tone and direction_sign. |
GET /v1/backdata | The news files and their state: rows, history_from, history_to, data_version, update_url. |
GET /v1/backdata/{ticker}.csv | The news history, one row a minute: time,score,item. With ?after= or ?since=, the update slice. |
GET /v1/backdata/{ticker}/meta | The file's meta. |
?res=hour | On /v1/live/{ticker}, /v1/backdata/{ticker}.csv, its /meta and the list: the same series on the hour (section Files and updates). Nothing is fetched or scored for it. |
GET /v1/backdata/{etf}/constituents?asof=YYYY-MM-DD | An ETF's constituents and weights on a date; /composition.json gives the whole month-by-month history. |
GET /v1/audit/{SYMBOL}.csv | Every headline ever scored for a company: published, score, qualifies, headline. |
GET /v1/brk | The breakout files and their state. |
GET /v1/brk/{ticker}.csv | The breakout history, one row an hour: hour,brk (an integer −10 … +10). Same update links. |
GET /v1/brk/{ticker}/latest | The last breakout row as JSON; it changes once an hour. |
GET /v1/brk/{ticker}/meta, GET /v1/brk/audit/{SYMBOL}.csv | The file's meta; the hour bars behind the breakout rows. |
POST /v1/subs/{ticker}, GET /v1/subs/{ticker}/status, GET /v1/subs/{ticker}/events, GET /v1/subs, DELETE /v1/subs/{ticker} | Create a ticker and watch it (section Create a ticker). |
POST /v1/alerts, GET /v1/alerts, DELETE /v1/alerts/{id}, GET /v1/alerts/{id}/log | Alerts (section Alerts). |
GET /v1/health | Jobs, lags, subs, disk: for monitoring. |
Accepted ticker forms: BTC, BTC-USD, X:BTCUSD; EURUSD, EUR-USD, EUR/USD; TQQQ and SOXL answer the QQQ and SOXX data (same basket, the leverage is not applied).
Files and updates
Download a file once, then ask every minute for what is new. Nothing you hold ever changes.
The news file: one row a minute
time,score,item
2026-10-17T10:43:00-04:00,-2.00,0
2026-10-17T10:44:00-04:00,4.20,1
2026-10-17T10:45:00-04:00,4.20,0
- The 10:44 row holds what was known at 10:43:50. The job runs at second 50 of every minute, writes the row for the next minute and appends it to the file in the same second; it is readable from 10:44:00.
item= 1 a new headline set this row, 0 carried forward (the same headline as the row before), 2 the current headline was revised and re-scored. Two headlines two minutes apart give two 1s; the same score from two different headlines gives two 1s. For an ETF, 1 means at least one constituent got a new headline that minute, 2 that one was revised.- Rows are never rewritten. A headline delivered late, or a revision, shows forward at the minute it was learned. The headline itself is in the live answer, in
?columns=extendedand in the audit file, not in the file. - Grid: stocks and ETFs 04:00 … 19:59 New York on NYSE trading days (960 rows a day); BTC every minute; FX pairs every minute from Sunday 17:00 to Friday 16:59.
The same file on the hour: ?res=hour
GET /v1/backdata/TSLA.csv?res=hour
time,score,item
2026-10-17T10:00:00-04:00,-2.00,0
2026-10-17T11:00:00-04:00,4.20,1
- For traders who work by the hour. The row labelled 11:00 is the 10:50 minute row: what was known at 10:49:50, and nothing later.
item= 1 a new headline arrived since the previous hour's reading, 2 only a revision did, 0 nothing.- One row is appended at every :50; the update link is the same with
&res=hour, andX-Next-Runsays when the next row is readable. Rows 05:00 … 20:00 for stocks and ETFs (readings 04:50 … 19:50), every hour for BTC, Sunday 18:00 to Friday 17:00 for FX. - It is a view of the minute file: nothing is fetched or scored for it, so the two can never disagree.
The breakout file: one row an hour
hour,brk
2026-10-17T10:00:00-04:00,0
2026-10-17T11:00:00-04:00,-3
The 11:00 row is the bar that closed at 10:50, built at :57. A 0 is the normal state: the score is non-zero only in an asset's most stretched hours. Rows 04:00 … 19:00 for stocks and ETFs, every hour for BTC, Sunday 17:00 to Friday 16:00 for FX.
Keeping a file up to date
- First time:
GET /v1/backdata/QQQ.csv(or/v1/brk/QQQ.csv). Keep theX-Data-Versionheader and your last row's time. The body comes gzipped when you sendAccept-Encoding: gzip;X-Content-SHA256is its checksum; byteRangerequests work for resumes. - Then every minute from MM:00 (news), or every hour from HH:59 (breakout):
200 → the rows strictly after your last one, one line a minute as a rule: append them. 204 → nothing newer yet (GET /v1/backdata/QQQ.csv?after=2026-10-17T10:44:00-04:00&version=2X-Next-Runsays when). 409 version_changed → the history was regenerated on purpose: download the whole file again.?since=<time>also exists and re-sends the row you name first, inclusive. - Never derive
afterfrom the clock, always from the last row you hold.
Options on any file: ?columns=extended (the inputs behind each row: article id, publication time, headline; contributors, coverage and snapshot for an ETF; the bar close for the breakout file) and ?excel=1 (BOM, CRLF, formulas neutralised).
Sizes
Live
GET /v1/live/{ticker} for any ticker, served or not: the newest headline tagged with it, scored at once.
{"ticker":"NVDA","score":4.2,"headline":"Nvidia shares rise on data-centre demand",
"published":"2026-10-17T13:52:11Z","as_of":"2026-10-17T13:52:11Z","age_seconds":97,
"stale":false,"cached":false,"source":"news","method":"tag"}
An index ETF has no single headline: its live value is its latest minute row, the mix over its constituents. An FX pair comes from the FX news source, with tone (the raw score) and direction_sign beside the signed score. There is no item in a live answer: nothing is kept between two calls.
GET /v1/live/{ticker}?res=hour answers the hourly reading of a served ticker instead: its latest :50 minute row, labelled with the hour it feeds (hour), with its item, the reading minute and next_hour, when the next one is due. It is read from the minute rows: nothing is fetched, nothing is scored.
The news score
Our AI model reads the headline and answers a sentiment from −10 to +10, with two decimals.
- Only articles tagged with at most 5 tickers and whose title names fewer than 4 tickers count, so market round-ups never make a row. A reprint of the same headline does not reset the row.
- Sources: institutional news wires for stocks, ETF constituents and crypto; specialised FX news desks for the currency pairs.
- ETF score: the constituents' scores over the last 24 hours, mixed by their index weight; a name without a recent headline counts as neutral. Weights are the SEC-filed holdings in force that day, rolled by price between filings, so membership changes apply through the whole history.
- FX pairs: the tone is signed for the pair's direction. A headline naming the pair or its base currency keeps its tone ("EUR/USD falls", "Euro climbs"); a headline about the quote currency alone is flipped ("Dollar surges" is negative for EUR/USD and GBP/USD, "Yen jumps" is negative for USD/JPY); a cross is signed by which side of it is ours.
The breakout score
An integer from −10 to +10 per hour, from the hour bars that close at :50 (extended hours 04:00–20:00 included; split-adjusted).
The score measures how far the price is stretched from its recent path, relative to the asset's own volatility, ranked against the asset's own last year. 0 is the normal state; the score is non-zero only in the asset's most stretched hours, and its sign is contrarian: a stretch up scores negative, a stretch down positive. For an ETF the stretch is mixed over the constituents by index weight and ranked against the basket's own year. A file starts about three months after its first bar, the history the ranking needs.
Create a ticker
Any single ticker: a US stock (AAPL), a coin (ETH or ETH-USD), an FX pair (AUDUSD or AUD/USD). Index ETFs built from constituents are refused with etf_needs_composition.
POST /v1/subs/AAPL→202 {job_id, status_url, events_url, csv_url}. One creation runs at a time: while one runs, another ticker gets409 creation_busywith the running ticker and its ETA. A ticker that already exists answers200 {state: "exists"}.- Watch it:
GET /v1/subs/AAPL/status(add?wait=60to long-poll) answers{state, step, articles, scored, pct, eta_s}, withstategoing queued → downloading → scoring → building → brk → done (or failed, witherror).GET /v1/subs/AAPL/eventsis a text/event-stream of the same object every few seconds until done. done= the minute file is on disk and served at/v1/backdata/AAPL.csv, the breakout file at/v1/brk/AAPL.csv, the audit file at/v1/audit/AAPL.csv, and the ticker is updated every minute from then on. Alerts can be set on it. The whole archive is downloaded and scored: a mid-cap takes 3 to 10 minutes, a large cap up to an hour.DELETE /v1/subs/AAPLstops the updates (the file stays);GET /v1/subslists the subs and the running job.
From Telegram the same thing is /create AAPL; the bot messages the chat when the ticker is ready.
Alerts
Single tickers only, never an ETF. On every minute row with item 1 or 2 the address receives the ticker, the minute, the score, the headline and its link.
POST /v1/alerts
{"ticker": "TSLA", "channel": "telegram", "address": "<chat id>", "min_abs_score": 5, "cooldown_s": 600}
- Telegram: the bot gives the chat id on
/start; or simply/sub TSLAin the chat, no API call needed. Active at once. - Email:
"channel": "email", "address": "you@example.com"; the address receives a confirmation mail from alerts@opearl.ai and the alert is active once the link is clicked. - The two filters are optional:
min_abs_scoresends only when |score| reaches the value;cooldown_ssends at most one message per that many seconds. GET /v1/alertslists them,DELETE /v1/alerts/{id}removes one,GET /v1/alerts/{id}/logshows the last deliveries: sent, pending, or failed after three attempts.
Errors and limits
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized, key_in_url | Missing or wrong key; a key in the URL. |
| 404 | not_subscribed, no_news, no_job | Not a served ticker; no headline for it; no creation requested. |
| 400 | bad_ticker, bad_since, version_required, etf_needs_composition, no_alerts_on_etf | The request itself. |
| 409 | version_changed, creation_busy, alert_exists | Re-download the file; a creation is running; the address already has that alert. |
| 503 | busy, upstream_unavailable, service_restarting | Try again after Retry-After seconds. |
Updates: the news rows are readable from MM:00, the breakout rows from HH:59; X-Next-Run on every file answer says when the next come. One creation runs at a time. Mail alerts are limited to a few hundred a day in total; Telegram has no limit that matters.
Request a key
The API needs a key. Tell us who you are and what you want to do with the feeds; you get an answer by mail.