M

MarketLens

AI market research that shows its work. Every number links back to the tool call that measured it, and if the data fails it refuses to write rather than guess.

The problem

You cannot check an AI answer about money

When you read one, nothing tells you which numbers came from real data and which the model produced on its own. Every figure looks equally confident. That is a verification problem, and it is solvable.

We found this in our own app first. Run with the network off and it happily reported a market cap of exactly 250 billion dollars and a P/E of 24.5, both invented by a fallback path. One function derived a stock's daily percent change from the letters in its ticker symbol.
How it works

A workflow, not a chat

Steps run as a graph in dependency order, with independent ones in parallel. The workflow decides which tools run, never the model, so the same question on the same day makes the same calls and a run can be replayed.

Fetch price history
192 ms · yfinance
DONE
Fetch fundamentals
447 ms · yfinance
DONE
Fetch headlines
220 ms · yahoo-rss
DONE
Compute indicators
10 ms · computed
DONE
Detect patterns
11 ms · computed
DONE
Score sentiment
0 ms · computed
DONE

Every figure is a receipt

Real output from a run against AAPL. Each number carries the id of the fact it came from, and hovering it in the app shows the provider and fetch time.

Generated by Featherlessevery figure verified
The current price of AAPL is $309.35f2, which is below its period high of $344.27f5 and above its period low of $223.86f6. The stock's last close is also below its SMA-20 of $314.20f27

Backed by a fact ledger

Everything a tool measures is registered before the model ever sees it. The model can only cite from this table.

IDMeasurementValueToolSource
f2prices.last_close$309.35pricesyfinance
f5prices.period_high$344.27pricesyfinance
f6prices.period_low$223.86pricesyfinance
f16fundamentals.pe_ratio35.48fundamentalsyfinance
f17fundamentals.eps$8.72fundamentalsyfinance
f26indicators.rsi_1447.4indicatorscomputed
f27indicators.sma_20314.2indicatorscomputed
f30indicators.volatility_pct32.48%indicatorscomputed

Then the memo gets checked

Asking a model to cite sources is close to worthless, because it will attach a convincing citation to a number it invented. So MarketLens reads its own finished memo back and checks every figure against the ledger.

verifiedmatches what we measured
unciteda number with no source
unknown_factcites an id that does not exist
mismatchcites something real, writes a different number
The part people remember

When the data fails, it says nothing

Give it a symbol that does not resolve. The pipeline goes red, dependent steps are skipped, and no memo is produced. There is a working language model available and unused.

Fetch price history
price history unavailable: empty response
FAILED
Fetch fundamentals
ticker info unavailable: no data
FAILED
Compute indicators
dependency did not complete
SKIPPED
Detect patterns
dependency did not complete
SKIPPED
No memo was writtenrun halted

A required step did not complete, so the workflow stopped rather than letting the model write around the gap. Any facts that were measured are still listed and can be inspected.

Run it

Locally in four commands

git clone https://github.com/aarushkik/ReverieStockPlatform
pip install -r requirements.txt
python -m auth.train
streamlit run app.py

MarketLens is a Streamlit server, so it needs a Python host and cannot run on this page. Set any one of FEATHERLESS_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, DEEPSEEK_API_KEY or GEMINI_API_KEY to enable memo writing. Without one it still runs the pipeline and collects facts, and tells you so.

The story

Inspiration

We were building a normal market terminal. Then one afternoon we ran it with the wifi off, mostly by accident, and it kept working.

Prices showed up. Fundamentals showed up. A market cap of exactly 250 billion dollars, a P/E of 24.5, a full table of top gainers and losers. All of it came from our own fallback code, in the same font and the same colour as the real thing, with nothing on screen to tell you the difference. One function was calculating a stock's daily percent change from the letters in its ticker symbol. It added up the character codes and did some arithmetic on them. That was the number a user would have read as a real price move.

Nobody wrote that to be dishonest. It happened the way it always happens, one reasonable looking except block at a time, each one just trying to keep the page from crashing.

But it made the real problem obvious. People are already making money decisions on AI answers they cannot check. The issue is not that models are dumb. It is that when you read an answer, nothing tells you which parts came from real data and which the model made up. Every number looks equally confident. We had built a small version of that exact problem without noticing, so we decided to go build the fix instead.

What it does

MarketLens is a market terminal where AI research runs as a workflow instead of a chat.

You pick what you want to know, and it runs a pipeline of real tools over real market data while you watch. Prices, indicators, fundamentals, news, sentiment. Then it writes you a memo where every single number is a receipt. Hover any figure and you see the exact tool call that measured it, which provider it came from, and how long ago.

The part we care most about is what happens when the model gets something wrong. We do not just ask it to cite sources, because a model will happily stick a convincing looking citation on a number it invented. Instead MarketLens reads its own finished memo back, pulls out every number in it, and checks each one against what the tools actually returned.

There are four possible outcomes for any figure:

StatusWhat it means
verifiedthe number matches what we measured
unciteda number with no source at all
unknown_factit cited an id that does not exist
mismatchit cited something real but wrote a different number

Anything that fails shows up flagged in the memo, in front of the reader, rather than being quietly fixed or hidden.

And if a data source goes down, the run stops and no memo gets written at all. Even with a perfectly good model sitting there waiting. It would rather tell you it does not know.

How we built it

Everything follows from one decision: the workflow picks the tools, not the model.

Each workflow is a graph of steps. The engine runs them in dependency order and fires independent ones in parallel, so fetching prices, fundamentals and news all happen at once. The language model never chooses what to call. Its only job is to reason over data it did not select.

We gave up some flexibility there and got something better in return. The same workflow on the same day makes the same calls, so a run can be replayed and audited. It also cuts out the flakiest part of most agent systems, which is the model wandering off and calling something strange.

Underneath sits the fact ledger. Every number a tool produces gets registered with an id, a value, a unit and where it came from. That table goes into the prompt, and the model is told it can only cite from it. Afterwards we compare each number in the memo against the fact it cited, allowing for honest rounding but nothing more. Writing 182.40 for a measured 182.3956 is fine. Writing 192.40 is not.

Then we had to go back and fix the data layer, because a ledger built on invented numbers would be worthless. Every fetcher now either returns something it actually measured or raises an error. Nothing returns a plausible substitute anymore.

On top of that we built a design system where every colour, size and spacing value comes from tokens, so the whole terminal reskins across four palettes and three densities with contrast guaranteed. And there is a sign in layer with trained models for spotting impossible travel between logins and detecting bots.

Challenges we ran into

Our first model scored 100 percent, and that was the bug. The login risk classifier came back with a perfect AUC of 1.0000. That is not a win, it is a warning. It meant the fake attackers and fake real users we generated were trivially different, and the model had found some giveaway instead of learning anything useful. We had made every attacker noisy and every legitimate user clean. Fixing it meant writing in the cases that actually trip real systems up: someone fumbling their password four times, a laptop bought while travelling, an attacker renting a residential IP, malware replaying a stolen device fingerprint. It landed at 0.966, which is a number we actually trust.

A real model found a bug that our fake ones never could. We tested the verifier for ages against stub models that wrote exactly the citations we expected. The first time we pointed a real 70B model at it, the model wrote "its SMA-20 of $314.20" and our verifier flagged the 20 as an unverified claim. It had read a parameter name as a claim about the market. Three angry red warnings on a memo that was completely correct. That taught us something uncomfortable: stubs only ever confirm what you already assumed.

Merging two branches that had both rewritten the same 3000 line file. Eighteen commits each way. What caught us out is that when you resolve a conflict in favour of one side, you drop the other side's definitions, but any code that uses them sitting in a quiet part of the file survives untouched. Git merges those without a word. Four broken references made it through and only blew up at runtime, one crash at a time. Running a linter over the merged result found all of them in a single pass, which is a lesson we will not forget.

Slow code hiding in plain sight. The RSI chart was recalculating the entire history for every single bar on the chart. Fixing it meant reproducing the original smoothing formula exactly so no chart in the app would silently change shape. It went from 51 milliseconds to 0.4, and we pinned it against the old version bit for bit to prove nothing moved.

Accomplishments that we're proud of

The moment where it refuses to answer. Type in a symbol that does not exist and watch the pipeline go red, the dependent steps get skipped, and the memo simply never appear. There is a working language model sitting right there, unused. Every demo we have shown, that is the part people remember.

We caught a real model lying, live. Not in a test with a rigged stub. We wrote a prompt deliberately telling the model to state a forward P/E, an analyst price target, revenue growth and a dividend yield, with exact numbers. It refused four times and said the data was not available. When we forced a bad number through anyway, the verifier flagged it as a mismatch and showed the reader.

Zero invented numbers left. We went through and tore out every fabrication we had: the fake company, the fake market movers, the fake insider trades that were attributed to real named executives, the machine learning model reporting a backtest accuracy it had never computed. You can grep the codebase for the tricks we used and find nothing.

158 tests, all offline, running in about a second. This project had zero tests when we started. The most valuable ones are the adversarial cases, where we hand the verifier a memo that cites a real fact but states the wrong number and make sure it gets caught every time.

We found a bug that had been silently broken forever. News fetching had been failing on every single request because of a certificate problem, and a bare except was turning that into an empty list. The app could not tell the difference between "no news today" and "our networking is broken", so neither could anyone using it.

What we learned

That a merge which compiles is not a merge that works. That a perfect score usually means you tested the wrong thing. That stub data agrees with you and real systems argue back, which is exactly why you need to plug the real thing in early.

But mostly we learned that honesty is something you build, not something you claim. Our verifier is only believable because we first went and ripped out every place the app was making things up. When someone asks how they know the data underneath is not lying too, we can point them at the commits instead of asking them to trust us.

What's next for MarketLens

Run history, so you can ask the same question a week later and see exactly which facts moved and which stayed put. Saved portfolios that survive a refresh. And pushing citation checking out of the workflow tab and into every corner of the app, so that no number anywhere appears without something real standing behind it.