[KongchangAI]
· 4 min read· 2,220 words

AI Never Writes Numbers: How Dual-Channel Rendering Eliminates Erroneous Financial Report Data

AI Never Writes Numbers: How Dual-Channel Rendering Eliminates Erroneous Financial Report Data

Stock Expert AI uses dual-channel rendering, a number gate, and hash verification to ensure the model never generates numbers on the page.

Facing the risk that large language models may fabricate numbers when summarizing financial reports, Stock Expert AI proposes "dual-channel rendering": the data channel renders all numbers verbatim from FMP, while the commentary channel has the model write only text. A number gate extracts every number in the draft and compares it against the fact pack with precision-based tolerance, rejecting anything unsupported. A second reviewer checks only a narrow checklist, and SHA-256 hashes ensure approved text and evidence aren't tampered with. This replaces statistical trust in model accuracy with a mechanically checkable promise.

Title

AI Never Writes Numbers: How Dual-Channel Rendering Eliminates Erroneous Financial Report Data

Meta Description

Stock Expert AI uses dual-channel rendering so the AI only writes commentary and never touches numbers—all financial data is rendered verbatim from FMP. Combined with a number gate, a second reviewer, and hash verification, this ensures the numbers on the page always come from the data source, not the model.

Article Content

When summarizing financial reports, large language models will eventually generate an "almost correct" revenue figure. The number looks plausible, the sentence reads smoothly, but nothing in the output tells you which parts were copied and which the model made up. For a site that scores stocks, this is the most fatal failure mode: a confidently wrong number looks exactly like every correct one.

Stock Expert AI has a hard rule for this problem: the model is never allowed to generate any numbers. They call it "dual-channel rendering." This article, based on the team's sharing in the developer community, breaks down the principles, implementation, and costs of this mechanism.

Dual Channel: Complete Separation of Data and Commentary

The rule is simple to state: the AI only writes commentary, and every number on the page is rendered verbatim from the market data provider (Financial Modeling Prep, FMP). The two channels have clear divisions of labor:

  • Data channel: values fetched from FMP, rendered by ordinary code. No model is involved between the provider response and the final pixel.
  • Commentary channel: text written by the model, responsible for explanation, context, and connecting information.

Dual-channel rendering diagram

The core value of separating the two is this: you no longer need to ask "is the model accurate?"—a question that can only be answered statistically; you start asking "did any number leak from the commentary channel onto the page?"—a question that can be checked mechanically. The model may err in emphasis or tone, but it cannot err on the numbers displayed on the page, because the page simply doesn't take numbers from it.

This covers the structured parts of the site, but not the text itself. If you let the model write freely, it will still produce sentences like "revenue grew to $46.06 billion." That's what the second half of the design solves.

FMP (Financial Modeling Prep) is a financial data API service for developers, providing structured data such as financial statements, analyst estimates, stock ratings, insider transactions, and earnings call transcripts. Its core value is that data is standardized and cleaned, returned in machine-readable formats like JSON rather than unstructured text obtained by web scraping. For applications like Stock Expert AI, FMP plays the role of "single source of truth": numbers such as revenue, margins, and EPS on the page are all taken directly from FMP responses, with no model processing in between. This means responsibility for data correctness is shifted entirely to the data provider; the team only needs to ensure the rendering code doesn't tamper with values, rather than assessing the statistical accuracy of model output.

Log Pipeline: Evidence First, Then the Article

The news section is where the model writes the most, so the mechanisms around it are the heaviest.

Topic selection is not decided by the model. Stories are picked from FMP data: earnings calendars, top gainers within two trading days (with a market cap floor of $1 billion), and sector themes. The same stock is not covered again within 7 days.

Each story builds a "fact pack" from FMP endpoints: financial statements, analyst estimates, ratings, business segments, insider transactions, and earnings call transcripts. Then a mid-sized model writes a draft using this fact pack as its only source.

Before anyone sees the draft, it must pass a gate.

Number Gate: Every Number Must Have a Source

The gate extracts every number in the draft and compares it against the fact pack, allowing rounding tolerance. If the draft says "$46.06 billion," it should match 46,061,000,000 in the fact pack, because the writer rounded to two decimals. If the draft says "$47 billion," it should not pass.

A clever trick: the tolerance is determined by how the number is written. If the draft shows two decimals in billions, the allowed error is half of the last digit's unit, scaled up proportionally. Simplified TypeScript pseudocode is as follows:

interface Parsed {
  value: number;
  tolerance: number;
}

const SCALE: Record<string, number> = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 };

function extractNumbers(text: string): Parsed[] {
  const out: Parsed[] = [];
  const re = /\$?(\d[\d,]*)(?:\.(\d+))?\s?([kmbt])?\b/gi;
  for (const m of text.matchAll(re)) {
    const whole = m[1].replace(/,/g, "");
    const frac = m[2] ?? "";
    const scale = SCALE[(m[3] ?? "").toLowerCase()] ?? 1;
    const value = Number(`${whole}.${frac || "0"}`) * scale;
    // half a unit in the last digit the writer chose to show
    const tolerance = 0.5 * 10 ** -frac.length * scale;
    out.push({ value, tolerance });
  }
  return out;
}

function findUnsupported(draft: string, facts: number[]): Parsed[] {
  return extractNumbers(draft).filter(
    (n) => !facts.some((f) => Math.abs(f - n.value) <= n.tolerance),
  );
}

// "$46.06B" -> 46.06e9 with tolerance 5e6, so 46_061_000_000 passes

This code is deliberately plain. The production gate also handles percentages, years, quarter labels, ranges, and words like "doubled." The key is the overall shape: extract everything that looks like a number, require that each one can be explained by the evidence, and reject outright on failure. Any unsupported number sends the draft back rather than being patched.

Reject-on-failure also has a useful side effect: the gate doesn't need to understand sentence meaning. It only needs to know that a number appeared and that nothing in the evidence can explain it.

Tolerance here means something different from ordinary numeric comparison: it is not a fixed absolute or relative error, but an allowed range derived backward from the display precision the writer chose. When the draft says "$46.06 billion," the last decimal place is 0.1 billion (i.e., $10 million), and the allowed error is half a unit, i.e., $5 million, so 46,061,000,000 in the fact pack can pass. The intuition behind this design is: since the writer chose to keep two decimals, they implicitly commit that rounding at that precision is correct. Tolerance scales with the unit (k/m/b/t) proportionally, so "4.6B" has a wider tolerance range than "4.61B." It transforms "is the number trustworthy" into "is the precision the writer claims consistent with the evidence," avoiding the burden of manually setting thresholds for every number.

Second Reviewer: Blind Spots Regex Can't See

The number gate catches numbers that don't exist, but it cannot judge whether a real number is attached to the wrong period, whether a sentence has slid into investment advice, or whether a quote has been distorted. So a larger model reviews the draft, but only against a narrow checklist: wrong numbers, wrong periods, investment advice, and distorted quotes. They don't ask this model "is the article good." A narrow checklist is easier to evaluate and harder for the reviewer to drift off course.

After an article passes, the site stores the SHA-256 hash of the approved text and the hash of the evidence. An article is indexable only when both hashes still match. If someone edits the text, or the underlying evidence changes, the article drops out of the indexable set until it is re-approved. This turns "verified at some point in time" into a property the system checks on every request.

SHA-256 hash is a cryptographic hash function that produces a fixed 256-bit (32-byte) output for any input, with an avalanche effect: changing even a single space in the input produces a completely different output. Here, the team computes SHA-256 separately for the approved text and its evidence, and stores both hashes. On each indexing request, the system recomputes the hashes of the current text and evidence, and the article is indexable only if both match the stored values. This automates "content integrity" verification: any text edit or evidence update causes a hash mismatch, and the article automatically drops out of the indexable set until it goes through the approval process again. Hash computation is extremely cheap and suitable for high-frequency checks, but it is sensitive to whitespace, so normalization rules (such as unifying line breaks and trimming leading/trailing spaces) need to be decided in advance to avoid false positives.

Long Transcripts and the Original-Source Rule

Earnings call transcripts are long, so a low-cost model summarizes them before the writer sees them. This introduces an obvious risk: the summarizer could fabricate or distort a number, and the writer would repeat it in good faith.

The solution is a rule of ordering: the number gate always runs against the original transcript, not the summary. If the summary introduces a number that doesn't exist in the source file, the gate has nothing to match it against, and the draft fails. Summary errors cannot introduce numbers into published text, because the summary is never the thing trusted.

The same idea applies to other lossy steps: whatever compresses or rewrites the evidence, verification must point to the uncompressed original file.

Lossy compression in this article's context refers to any intermediate step that loses or distorts original information, such as summarizing a long transcript with a low-cost model, condensing the fact pack, or reformatting evidence. The team's principle is: verification must always point to the uncompressed original file (such as the full transcript), not the compressed product. The reason is that the compression step itself may introduce errors (such as the summarizer fabricating numbers); if the gate verifies only against the summary, it would treat the summary's errors as the factual baseline and thus let false numbers through. This rule can be generalized to any "compress-then-use" pipeline: as long as the final output depends on some intermediate representation, verification must trace back to the source of that representation, otherwise the compression step becomes the weak link in the trust chain.

Service Layer: HTML for Crawlers, Markdown for AI Crawlers

Crawlers get server-rendered HTML. Each article also has a plain Markdown copy at the same URL plus .md, aimed at AI crawlers. The stack is React + Vite + TypeScript on the frontend, Express + TypeScript on the backend, Drizzle ORM with Neon PostgreSQL, a Cloudflare Worker in front, and hosting on a Replit VM. None of this is unusual. The key point is that the Markdown copy comes from the same approved text as the HTML, so both fall within the hash check.

All model calls run headlessly on a flat subscription, and they pick the cheapest sufficient model for each task: cheap for summaries, mid-tier for writing, larger for review.

Trade-offs and Reflections

The writing became plainer. A model that can't cite numbers outside the fact pack writes fewer vivid comparisons. Some drafts were rejected because the numbers were technically true but derived values, such as growth rates the model calculated itself. They treat derived numbers as the model doing arithmetic, which is exactly what the rule forbids, so the fix is to compute them in code and put them into the fact pack.

The gate is only as good as the fact pack. If FMP lacks a field, a true statement will also be rejected by the gate. They accept false rejections over false approvals, but this does affect throughput.

Extraction is the most fragile part. Regex-based number extraction requires ongoing maintenance around formats. If starting over, they would define a structured representation of numbers earlier, having the writer output labeled placeholders filled in by code with values, rather than reverse-parsing numbers from free text. That would make the two channels even stricter than they are now.

Hashing is cheap but unforgiving. Any whitespace-level change to the approved text invalidates it. That's exactly the behavior they want, but it means normalization rules need to be decided in advance.

Key Takeaways

The core idea is small: don't entrust to the model the things you can't afford to get wrong, and verify against the original source rather than another model's summary. A number gate, a narrow-scope reviewer, and a pair of hashes aren't complex, but together they make a checkable promise: the numbers on the page come from the data provider, not the model.

One-line Summary

Stock Expert AI uses dual-channel rendering, a number gate, and hash verification to ensure the model never generates numbers on the page.

Paragraph Summary

Facing the risk that large language models may fabricate numbers when summarizing financial reports, Stock Expert AI proposes a "dual-channel rendering" mechanism: the data channel renders all numbers verbatim from FMP, while the commentary channel has the model write only text. To close the gap where numbers could still appear in the text, they designed a number gate that extracts every number in the draft and compares it against the fact pack with tolerance based on written precision, rejecting anything unsupported. A second reviewer checks only a narrow checklist (wrong numbers, periods, investment advice, distorted quotes), and SHA-256 hashes ensure approved text and evidence aren't tampered with. Long transcripts are summarized first, but the gate always runs against the original source. This combination replaces statistical trust in model accuracy with a mechanically checkable promise, at the cost of plainer writing, dependence on fact pack completeness, fragile regex extraction, and hash sensitivity to whitespace.

#dual-channel rendering#number gate#FMP#SHA-256 hash verification#LLM hallucination#financial data pipeline
Share:

Related articles