← Back to all posts

PreBell: market news where code checks every number

9 min read

PreBell publishes US market news in English and Chinese with nobody writing it: a brief before the open, a recap after the close, and a short earnings check when a large company files its results. It's live at prebell.site, 盘前铃 in Chinese.

A language model writes the articles. The part I care about is everything around the model: code that decides what it is allowed to say, and refuses to publish when a number or a reason can't be traced to its source.

Where it started

It started with a question. A Chinese-language investing site I came across publishes market articles every day, on a schedule nobody keeps by hand, and I wanted to know how that could be built. Then I wanted to know something harder: could I build one where a reader could trust the numbers?

That second question became the project. Getting a model to write "Nasdaq futures fell 0.4% as chip stocks slid" is easy. The work is making sure the 0.4% is the real number, that chip stocks really slid, and that the word "as" is a claim something supports.

I started on October 29, 2026. The first pages went live a week later.

What it publishes

  • A premarket brief at 8:00 AM Eastern every trading day, rebuilt at 8:35 with the 8:30 economic data: index futures, the biggest premarket movers and why they moved, the sectors moving together, and what to watch.
  • An after-close recap: how the indexes closed, and the day's movers and themes.
  • Earnings checks: a two-minute read on a company's quarter, built from its SEC filing. 139 companies get one: the S&P 100 plus everything above $100 billion in market value.
  • A market calendar: earnings dates for 561 US-listed companies, checked against new SEC filings every five minutes and ticked once a company has reported, next to Fed decisions, the jobs report, CPI and election dates.

Everything comes out in English and Simplified Chinese. The two versions are written separately from the same facts, not translated from each other.

PreBell's market calendar: a week of earnings reports as company cards, with a tick on each one that has filed and the CPI release at the top of its day

The model writes, code decides

Each run has two halves. First, code gathers the facts: quotes, news articles, the filing, the economic release. They are stored as one bundle. Then the model gets that bundle and nothing else, and writes the article from it.

Before anything is published, code reads the draft back and checks it.

  • Every number must be in the facts. The checker pulls each number out of the draft and matches it against the bundle. Rounding is allowed, so $102,466 million can be written as $102.5 billion. Working out a new number is not: a growth rate the model computed by itself gets the draft rejected.
  • Every reason needs a quote. When a brief says a stock rose on a product launch, the facts hold the article's link and a sentence copied from it, and code confirms the sentence really appears in that article. No quote, no reason.
  • Some claims are banned outright. An earnings check may not say a company "beat" or "missed" anything. I don't have analyst estimates I'm allowed to use, and a model guessing at them is exactly the failure I'm building against.

A rejected draft goes back to the model once, with the reason. If it fails again, the run doesn't publish and tries again a few minutes later. Publishing nothing is always an allowed outcome.

Earnings, from the filing

Every five minutes on trading days, a Cloudflare Worker reads SEC EDGAR's feed of new filings. When one of the 139 write-up companies files its results, PreBell fetches the filing and its press release, pulls out the quarter's figures, and adds earlier quarters from the company's XBRL data for the charts.

Each figure carries the exact text it came from. Revenue of $14.92 billion is stored with the line of the release that states it, and the figure is kept only if that line really is in the document.

An earnings check from PreBell's first week: Charter Communications, with revenue and earnings per share drawn as a chart and a badge saying every figure is traced to the filing

That page is from the first week, and its headline is the first thing I'd fix. More on that below.

What the first week got wrong

Running it on real mornings showed me problems my tests never would.

  • Six of the fourteen earnings reports it detected in its first days didn't publish. Real press releases are messier than my test files: a table cell printed without its dollar sign, two companies that state no revenue at all, releases too large to read in one go, and a launch backlog that pushed some builds past their retry window. I fixed those before adding more companies.
  • On November 10, six of the brief's eight movers said "No clear news", including GE Vernova, down 7%. The same page showed its whole sector falling. The system knew something true and said nothing.
  • That morning's 8:35 update ran five minutes after the CPI report and showed a link to it, not the number.
  • Earnings headlines were the press release's own title. Charter had swung to a loss of $20.50 a share, and the headline read "Charter Communications Reports Third Quarter 2026 Results".

None of these was a wrong number. Each was a page that was accurate and not much use, which a number checker will never catch.

Saying only what can be proven

The "No clear news" problem is the one I learned the most from, because the obvious fix is wrong. The obvious fix is to let the model explain the move, and a model asked why a stock fell will always have an answer.

So the fallback is written by code, from things it can prove:

  • If the company reported results before this session, say so and link to the filing.
  • Otherwise, if its sector is one of the day's themes and the stock moved the same way, say "Fell with the networking & optical group (median −4.63%); no company news found". That says the stocks moved together. It doesn't say why. For now the line appears in the English brief only.
  • Otherwise, "No clear news".

Even those sentences needed care. An early version printed "Reported results after Monday's close" whenever the filing arrived after the close. But a filing's timestamp only proves the results were public by then, not when they came out: a company can publish at 7 AM and file at 4:30 PM. So that sentence now needs the filing time and the calendar to agree, and when they don't, the line isn't printed. And "no company news found" is dropped when the news search itself failed, because then nothing was searched.

The CPI number went the same way. Code now reads the figure straight from the Bureau of Labor Statistics release, along with the sentence it came from. The old check fetched only the first 16 KB of that page, enough to confirm the release existed, and the headline sentence sits about 34 KB in. Since I have no forecasts to compare against, a draft that calls the number "hotter than expected" is rejected, and the 8:00 brief stays up untouched until a clean update replaces it.

Earnings headlines now have to state the quarter's takeaway with a figure from the facts. A press-release title is rejected and the model tries again.

Running on a free plan

PreBell runs on Cloudflare's free Workers plan, and its limits shaped the design more than any framework did. One invocation gets 50 outbound requests, and an account gets five scheduled triggers. So nothing runs as one long job. Two cron triggers fire every five minutes, a small clock module decides what that moment means in New York time, and the work is cut into small queue messages, so no single invocation needs more than its share of requests.

The generator is Rust compiled to WebAssembly. All of the logic lives in a plain Rust library that knows nothing about Cloudflare, so its 801 tests run natively in a couple of seconds, with network calls replaced by fixtures. The site is Next.js, also on Workers, reading from D1 and R2.

Choosing the model by measurement

The model is swappable, and I picked it with a test. I replayed 18 of the first week's fact bundles through three models and counted how many drafts passed every check on the first try: Claude Sonnet 96%, DeepSeek 92%, Claude Opus 89%. In Chinese the gap between the first two was wider, 94% against 86%. Sonnet broke no rules where DeepSeek broke four, and Opus cost 2.5 times as much without being more accurate. At the current volume the model bill is about $23 to $32 a month.

How it was built

I built PreBell with coding agents, and I'd rather say how than leave it out. I set the rules, made the product decisions and kept them in a decisions log. Agents wrote most of the Rust, often several at once on separate parts. What made that workable is one rule: no change is approved by the agent that wrote it. A separate reviewer reads the diff cold and tries to break it.

The fixes above went through four review rounds. The reviewer found a share-price move that could be measured from before the report it was meant to react to, a wording check so broad it would have blocked ordinary sentences, and the timing sentences the data couldn't prove. None of those would have failed a test, because the tests were written by the same builders who made the mistakes.

Where it stands

PreBell is about two weeks old and has been live for one. It doesn't compare results with analyst expectations yet, which is the first thing most readers want from an earnings story, and that needs estimates I'm allowed to use. The after-close recap doesn't carry the economic figures yet. And I'm only starting to find out whether people want to read it, which matters more than any of the engineering above.

Try it

Open prebell.site around 8:00 AM Eastern on a trading day for the brief, or pick a ticked company on the market calendar. The Chinese version is at prebell.site/zh.